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 promiseReducer(state = {}, { type, name, status, payload, error }) {
if (type === "PROMISE") {
return {
...state,
[name]: { status, payload, error },
};
}
return state;
}
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 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));
}
};
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 backendURL = "http://shop-roles.asmer.fs.a-level.com.ua";
const gql = getGQL(backendURL + "/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 {
_id name
}
parent {
_id name
}
}
}`,
{ q: JSON.stringify([{ _id }]) }
)
);
const actionGoodById = (_id) =>
actionPromise(
"goodById",
gql(
`query goodById($good: String){
GoodFindOne(query: $good){
_id name description price categories{_id name owner{_id login nick}}images{url}
}
}`,
{ good: JSON.stringify([{ _id }]) }
)
);
const actionOrders = () =>
actionPromise(
"orders",
gql(`
query orders {
OrderFind(query: "[{}]") {
_id
total
createdAt
orderGoods {
price
count
total
good {
name images {
url
}
categories {
name
}
}
}
}
}`)
);
const jwtDecode = (token) => {
try {
const payload = JSON.parse(atob(token.split(".")[1]));
// серединка, atob, JSON.parse
return payload;
} catch (e) {
console.log(e);
}
};
function authReducer(state, { type, token }) {
if (state === undefined && localStorage.authToken) {
token = localStorage.authToken;
type = "AUTH_LOGIN";
}
if (type === "AUTH_LOGIN") {
let decodeToken = jwtDecode(token);
if (decodeToken) {
localStorage.authToken = token;
return {
token,
payload: decodeToken,
};
}
}
if (type === "AUTH_LOGOUT") {
localStorage.authToken = "";
//чистим localStorage.authToken
return {};
}
return state || {};
}
//написать к этому пару экшонов
const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
function cartReducer(state = {}, { type, good = {}, count = 1 }) {
//каков state:
//{
// _id1: {count:1, good: {_id1, name, price, images}}
// _id2: {count:1, good: {_id2, name, price, images}}
//}
//каковы действия по изменению state
if (type === "CART_ADD") {
count = +count;
if (!count) return state;
else
return {
...state,
[good._id]: { good, count: count + (state[good._id]?.count || 0) },
};
}
if (type === "CART_CHANGE") {
count = +count;
if (!count) return state;
return {
...state,
[good._id]: { good, count },
};
}
if (type === "CART_DELETE") {
const { [good._id]: removedProperty, ...someGoods } = state;
return someGoods;
}
if (type === "CART_CLEAR") {
return {};
}
return state;
}
const actionCartAdd = (good, count = 1) => ({ type: "CART_ADD", good, count });
const actionCartChange = (good, count = 1) => ({
type: "CART_CHANGE",
good,
count,
});
const actionCartDelete = (good) => ({ type: "CART_DELETE", good });
const actionCartClear = () => ({ type: "CART_CLEAR" });
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: orderGoods } }
)
)
);
if (result?._id) {
dispatch(actionCartClear());
}
};
const combineReducers =
(reducers) =>
(state = {}, action) => {
const newState = {};
for (const [reducerName, reducer] of Object.entries(reducers)) {
let newSubState = reducer(state[reducerName], action);
// console.log(newSubState)
if (newSubState !== state[reducerName]) {
newState[reducerName] = newSubState;
// console.log(newState[reducerName])
}
}
if (Object.keys(newState).length !== 0) return { ...state, ...newState };
else return state;
};
const store = createStore(
combineReducers({
promise: promiseReducer,
auth: authReducer,
cart: cartReducer,
})
);
// для корневых категорий
store.dispatch(actionRootCats());
store.subscribe(() => console.log(store.getState()));
const actionFullLogin = (login, password) => async (dispatch) => {
let token = await dispatch(
actionPromise(
"auth",
gql(
` query login($login:String, $password:String){
login(login:$login, password:$password)} `,
{ login, password }
)
)
);
if (token) {
dispatch(actionAuthLogin(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 tokenCheck = await dispatch(actionRegister(login, password));
if (tokenCheck?.login === login) {
dispatch(actionFullLogin(login, password));
}
};
store.subscribe(() => {
const { rootCats } = store.getState().promise;
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().promise;
const { cart } = store.getState();
const [, route, _id] = location.hash.split("/");
//проверка на наличие 'category' в адресной строке
if (catById?.payload && route === "category") {
//достаем имя
const { name } = catById.payload;
main.innerHTML = `
${name}
`;
if (catById?.payload?.subCategories) {
for (const { _id, name } of catById.payload?.subCategories) {
const link = document.createElement("a");
link.href = `#/category/${_id}`;
link.innerText = name;
main.append(link);
}
}
for (const myGood of catById.payload.goods) {
const { _id, name, price, images } = myGood;
const card = document.createElement("div");
card.innerHTML = `${name}
Цена ${price} грн
Перейти на товар ${name} `;
let btnAdd = document.createElement("button");
btnAdd.innerText = "Добавить в корзину";
card.append(btnAdd);
let btnDelete = document.createElement("button");
btnDelete.innerText = "Удалить из корзины";
btnDelete.classList.add("deleteBtn");
card.append(btnDelete);
let p = document.createElement("p");
if (cart[myGood._id]?.count != undefined)
p.innerHTML = `Выбранное количество: ${cart[myGood._id]?.count}`;
else p.innerHTML = `Выбранное количество: 0`;
card.append(p);
// console.log(cart[myGood._id]?.count)
//console.log(count)
btnAdd.onclick = () => {
store.dispatch(actionCartAdd(myGood));
};
btnDelete.onclick = () => {
store.dispatch(actionCartDelete(myGood));
};
main.append(card);
}
if (catById.payload?.parent && catById.payload?.parent != null) {
const { _id, name } = catById.payload.parent;
const linkParent = document.createElement("a");
linkParent.href = `#/category/${_id}`;
//console.log(_id, name);
linkParent.innerText = ` Вернуться к категории ` + name;
main.append(linkParent);
}
}
});
store.subscribe(() => {
const { goodById } = store.getState().promise;
const { cart } = store.getState();
const [, route, _id] = location.hash.split("/");
//проверка на наличие 'good' в адресной строке
if (goodById?.payload && route === "good") {
main.innerHTML = "";
//достаем имя
const { _id, name, description, price, images } = goodById.payload;
main.innerHTML = `${name}
${description}
Цена ${price} грн `;
let btnAdd = document.createElement("button");
btnAdd.innerText = "Добавить в корзину";
btnAdd.onclick = () => store.dispatch(actionCartAdd(goodById.payload));
main.append(btnAdd);
let p = document.createElement("p");
//console.log(goodById.payload._id);
if (cart[goodById.payload._id]?.count != undefined)
p.innerHTML = `Выбранное количество: ${
cart[goodById.payload._id]?.count
}`;
else p.innerHTML = `Выбранное количество: 0`;
main.append(p);
if (goodById.payload?.categories) {
for (const { _id, name } of goodById.payload.categories) {
const link = document.createElement("a");
link.href = `#/category/${_id}`;
link.innerText = `Вернуться к категории ${name}`;
main.append(link);
}
}
}
});
let pMess = document.createElement("p");
store.subscribe(() => {
const { auth } = store.getState();
if (Object.keys(auth).length !== 0) {
btnSignIn.innerHTML = `${auth.payload.sub.login}`;
}
const [, route, _id] = location.hash.split("/");
if (route === "login") {
pMess.innerHTML = "";
if (Object.keys(auth).length !== 0) {
if (
actionFulfilled(auth, auth.payload != null) &&
actionAuthLogin(auth)
) {
pMess.innerHTML =
"Вы авторизировались! Добро пожаловать, " +
`${auth.payload.sub.login}, в наш магазин!`;
pMess.style.backgroundColor = "#98FB98";
pMess.style.color = "#006400";
main.prepend(pMess);
}
} else {
btnSignIn.innerHTML = "Sign In";
pMess.innerHTML = "Имя пользователя или пароль неверны.";
pMess.style.backgroundColor = "#FA8072";
pMess.style.color = "#8B0000";
main.prepend(pMess);
}
}
});
store.subscribe(() => {
const { auth } = store.getState();
const [, route, _id] = location.hash.split("/");
if (route === "register") {
pMess.innerHTML = "";
if (Object.keys(auth).length !== 0) {
if (
actionFulfilled(auth, auth.payload != null) &&
actionAuthLogin(auth)
) {
pMess.innerHTML = `Вы успешно зарегистрированы!Добро пожаловать, ${auth.payload.sub.login}, в наш магазин!`;
pMess.style.backgroundColor = "#98FB98";
pMess.style.color = "#006400";
btnSignIn.innerHTML = `${auth.payload.sub.login}`;
main.prepend(pMess);
}
} else {
btnSignIn.innerHTML = "Sign In";
pMess.innerHTML =
"Такое имя пользователя уже существует, придумайте другое!";
pMess.style.backgroundColor = "#FA8072";
pMess.style.color = "#8B0000";
main.prepend(pMess);
}
}
});
store.subscribe(() => {
const { orders } = store.getState().promise;
const [, route, _id] = location.hash.split("/");
if (route === "dashboard") {
main.innerHTML = "";
let h2 = document.createElement("h2");
if (orders != undefined && orders?.payload?.length != 0) {
//console.log(orders);
h2.innerHTML = "Ваши заказы: ";
main.append(h2);
for (let elem in orders?.payload) {
const card = document.createElement("div");
card.classList.add("cart");
let num = document.createElement("p");
num.innerHTML = `№${elem} заказа
Ваши товары:
`;
card.append(num);
//console.log(orders?.payload[elem].total)
for (let elem2 in orders?.payload[elem]?.orderGoods) {
card.innerHTML += ` * ${orders?.payload[elem]?.orderGoods[elem2].good?.name}
Цена: ${orders?.payload[elem]?.orderGoods[elem2]?.price}
Количество: ${orders?.payload[elem]?.orderGoods[elem2]?.count}
`;
}
main.append(card);
let num2 = document.createElement("p");
num2.innerHTML = ` Общая сумма заказа: ${orders?.payload[elem].total} `;
card.append(num2);
}
} else {
h2.innerHTML = "У вас нету еще оформленных заказов! :( ";
main.append(h2);
}
}
});
let btnSignIn = document.getElementById("signIn");
let btnlogOut = document.getElementById("logOut");
window.onhashchange = () => {
const [, route, _id] = location.hash.split("/");
const routes = {
category() {
store.dispatch(actionCatById(_id));
},
good() {
store.dispatch(actionGoodById(_id));
},
login() {
main.innerHTML = "";
let labelLog = document.createElement("label");
labelLog.innerHTML = "Login";
let inputLogin = document.createElement("input");
let labelPass = document.createElement("label");
labelPass.innerHTML = "Password";
let inputPassword = document.createElement("input");
let sign = document.createElement("button");
sign.innerHTML = "SIGN";
sign.setAttribute("id", "sign");
pMess.innerHTML = "";
main.append(labelLog);
main.append(inputLogin);
main.append(labelPass);
main.append(inputPassword);
main.append(sign);
sign.addEventListener("click", () => {
try {
if (inputLogin.value != "" && inputPassword.value != "") {
store.dispatch(
actionFullLogin(inputLogin.value, inputPassword.value)
);
} else {
pMess.innerHTML = "Введите значение!";
pMess.style.backgroundColor = "#FA8072";
pMess.style.color = "#8B0000";
main.prepend(pMess);
}
} catch (e) {
console.log("myError", e);
}
});
},
register() {
main.innerHTML = "";
let labelLog = document.createElement("label");
labelLog.innerHTML = "Login";
let inputLogin = document.createElement("input");
let labelPass = document.createElement("label");
labelPass.innerHTML = "Password";
let inputPassword = document.createElement("input");
let register = document.createElement("button");
register.innerHTML = "REGISTER";
pMess.innerHTML = "";
main.append(pMess);
main.append(labelLog);
main.append(inputLogin);
main.append(labelPass);
main.append(inputPassword);
register.addEventListener("click", () => {
try {
if (inputLogin.value != "" && inputPassword.value != "") {
store.dispatch(
actionFullRegister(inputLogin.value, inputPassword.value)
);
} else {
pMess.innerHTML = "Введите значение!";
pMess.style.backgroundColor = "#FA8072";
pMess.style.color = "#8B0000";
}
} catch (e) {
console.log("myError", e);
}
});
main.append(register);
},
logout() {
main.innerHTML = "";
store.dispatch(actionAuthLogout());
btnSignIn.innerHTML = "Sign In";
},
cart() {
const { cart } = store.getState();
main.innerHTML = "";
let label = document.createElement("h2");
label.innerHTML = "";
main.append(label);
if (Object.keys(cart).length !== 0) {
label.innerHTML = "Ваши товары: ";
for (let good in cart) {
let {
good: {
_id: _id,
name: name,
price: price,
images: [{ url }],
},
count,
} = cart[good];
const card = document.createElement("div");
card.classList.add("cart");
card.innerHTML = `
${name}
Перейти на товар ${name}
`;
let inputCount = document.createElement("input");
inputCount.setAttribute("type", "number");
inputCount.value = `${count}`;
inputCount.min = 1;
let strongPrice = document.createElement("strong");
strongPrice.innerHTML = ` Цена ${price * inputCount.value} грн `;
inputCount.oninput = function () {
store.dispatch(actionCartChange(cart[good].good, inputCount.value));
strongPrice.innerHTML = ` Цена ${price * inputCount.value} грн `;
};
inputCount.classList.add("count");
let btnDelete = document.createElement("button");
btnDelete.innerHTML = "Удалить товар";
btnDelete.classList.add("deleteBtn");
btnDelete.addEventListener("click", () => {
store.dispatch(actionCartDelete(cart[good].good));
card.innerHTML = "";
});
inputCount.oninput = function () {
store.dispatch(actionCartChange(cart[good].good, inputCount.value));
strongPrice.innerHTML = ` Цена ${price * inputCount.value} грн `;
};
card.append(strongPrice);
card.append(inputCount);
card.append(btnDelete);
main.append(card);
}
let btnClear = document.createElement("button");
btnClear.innerHTML = "Очистить все товары";
btnClear.classList.add("clear");
let btnOrder = document.createElement("button");
btnOrder.innerHTML = "Оформить заказ";
btnOrder.classList.add("order");
btnOrder.addEventListener("click", () => {
alert("Ваш заказ оформлен! Спасибо что выбираете наш магазин!");
main.innerHTML = "";
label.innerHTML = "Ваша корзина пустая!";
main.append(label);
store.dispatch(actionOrder());
});
btnClear.addEventListener("click", () => {
store.dispatch(actionCartClear());
main.innerHTML = "";
label.innerHTML = "Ваша корзина пустая!";
main.append(label);
});
main.append(btnClear);
main.append(btnOrder);
} else {
label.innerHTML = "Ваша корзина пустая!";
}
},
dashboard() {
store.dispatch(actionOrders());
},
};
if (route in routes) routes[route]();
};
window.onhashchange();