123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741 |
- 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}){
- ////?????
- //ОДИН ПРОМИС:
- //состояние: PENDING/FULFILLED/REJECTED
- //результат
- //ошибка:
- //{status, payload, error}
- //{
- // name1:{status, payload, error}
- // name2:{status, payload, error}
- // name3:{status, payload, error}
- //}
- if (type === 'PROMISE'){
- return {
- ...state,
- [name]:{status, payload, error}
- }
- }
- return state
- }
- const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
- const actionFulfilled = (name, payload) => ({type: 'PROMISE', status: 'FULFILLED', name, payload})
- const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
- const actionPromise = (name, promise) =>
- async dispatch => {
- try {
- dispatch(actionPending(name))
- let payload = await promise
- dispatch(actionFulfilled(name, payload))
- return payload
- }
- catch(e){
- dispatch(actionRejected(name, e))
- }
- }
- const goToMainPage = () => location.href = location.href.split("#")[0];
- const checkAuthToken = () => {
- const headers = {
- "Content-Type": "application/json",
- "Accept": "application/json",
- }
- if(localStorage.getItem('authToken')) {
- return {
- ...headers,
- "Authorization": `Bearer ${localStorage.getItem('authToken')}`
- }
- } else {
- return headers;
- }
- }
- const getGQL = url =>
- (query, variables= {}) =>
- fetch(url, {
- method: 'POST',
- headers: checkAuthToken(),
- body:JSON.stringify({query, variables})
- }).then(res => res.json())
- .then(data => {
- try {
- if(!data.data && data.errors) {
- throw new SyntaxError(`SyntaxError - ${JSON.stringify(Object.values(data.errors)[0])}`);
- }
- return Object.values(data.data)[0];
- } catch (e) {
- console.error(e);
- }
- });
- function jwtDecode(token){
- try {
- return JSON.parse(atob(token.split('.')[1]))
- }
- catch(e){
- }
- }
- function authReducer(state={}, {type, token}){
- //{
- // token,payload (раскодированный токен)
- //} или, если не залогинены
- //{
- // нихрена, т. .е. пустой объект
- //}
- if (type === 'AUTH_LOGIN'){ //то мы логинимся
- const payload = jwtDecode(token)
- if (payload){
- return {
- token,
- payload
- }
- }
- }
- if (type === 'AUTH_LOGOUT'){ //мы разлогиниваемся
- return {}
- }
- return state
- }
- const actionAuthLogout = () =>
- dispatch => {
- dispatch({type: 'AUTH_LOGOUT'});
- localStorage.removeItem('authToken');
- goToMainPage()
- }
- const actionAuthLogin = (token) =>
- (dispatch, getState) => {
- const oldState = getState()
- dispatch({type: 'AUTH_LOGIN', token})
- const newState = getState()
- if (oldState !== newState)
- localStorage.setItem('authToken', token)
- }
- function cartReducer(state={}, {type, amount=1, good}){
- /*
- {
- id1: {amount, good: {объект с бэка с _id, description, name, price}},
- id2: {amount, good: {объект с бэка с _id, description, name, price}},
- id3: {amount, good: {объект с бэка с _id, description, name, price}}
- }
- */
- if (type === 'CART_ADD'){
- return {
- ...state,
- [good._id]: {good, amount: (state[good._id]?.amount || 0) + amount }
- }
- }
- if (type === 'CART_SET'){
- return {
- ...state,
- [good._id]: {good, amount}
- }
- }
- if (type === 'CART_DELETE'){
- const {[good._id]: skip,...newState} = state
- //const newState = { ...state }
- //delete newState[good._id]
- return newState
- }
- if (type === 'CART_CLEAR'){
- return {}
- }
- //if (type === ''){
- //}
- return state
- }
- const actionCartAdd = (good, amount=1) => ({type: 'CART_ADD', good, amount})
- const actionCartSet = (good, amount=1) => ({type: 'CART_SET', good, amount})
- const actionCartClear = () => ({type: 'CART_CLEAR'})
- const actionCartDelete = (good) => ({type: 'CART_DELETE', good})
- function combineReducers(reducers){
- function totalReducer(state={}, action){
- //{
- //promise:{
- //name1:{status, payload, error},
- //name2:{status, payload, error}
- //},
- //auth: {
- //token, payload
- //}
- //}
- const newTotalState = {}
- for (const [reducerName, reducer] of Object.entries(reducers)){
- const newSubState = reducer(state[reducerName], action)
- if (newSubState !== state[reducerName]){
- newTotalState[reducerName] = newSubState
- }
- }
- if (Object.keys(newTotalState).length){
- return {...state, ...newTotalState}
- }
- return state
- }
- return totalReducer
- }
- function localStoredReducer(reducer, localStorageKey){
- function wrapperReducer(state, action){
- if (state === undefined){ //если загрузка сайта
- try {
- return JSON.parse(localStorage[localStorageKey]) //пытаемся распарсить сохраненный
- //в localStorage state и подсунуть его вместо результата редьюсера
- }
- catch(e){ } //если распарсить не выйдет, то код пойдет как обычно:
- }
- const newState = reducer(state, action)
- localStorage.setItem(localStorageKey, JSON.stringify(newState)) //сохраняем состояние в localStorage
- return newState
- }
- return wrapperReducer
- }
- const reducers = {
- auth: authReducer,
- cart: localStoredReducer(cartReducer, 'cart'), //в localStorage должен появится ключ cart с JSON стейта корзины
- promise: localStoredReducer(promiseReducer, 'promise'),
- }
- const totalReducer = combineReducers(reducers)
- const store = createStore(totalReducer) //не забудьте combineReducers если он у вас уже есть
- store.subscribe(() => console.log(store.getState()))
- const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/'
- const gql = getGQL(backendURL + 'graphql')
- const rootCategories = () =>
- gql(`query cadz($q:String) {
- CategoryFind(query:$q){
- _id name
- }
- }`, {q: JSON.stringify([{parent: null}])})
- const actionRootCategories = () =>
- actionPromise('rootCategories', rootCategories())
- const categoryById = _id => //добавьте сюда подкатегории и родителя - пригодятся
- gql(`query catById($qCat:String){
- CategoryFindOne(query:$qCat){
- _id name
- parent {
- _id name
- }
- subCategories {
- name _id parent {
- _id name
- }
- }
- goods{
- _id name price images{
- url
- }
- }
- }
- }`, {qCat: JSON.stringify([{_id}])})
- const actionCategoryById = _id =>
- actionPromise('catById', categoryById(_id))
- const goodById = _id =>
- gql(`query goodById($goodId:String) {
- GoodFindOne(query:$goodId) {
- _id name price description images {
- url
- }
- }
- }`, {goodId: JSON.stringify([{_id}])})
- const actionGoodById = _id =>
- actionPromise('goodById', goodById(_id));
- const actionOrder = () =>
- async (dispatch, getState) => {
- const order = Object.values(getState().cart).map(orderGoods => ({good: {_id: orderGoods.good._id}, count: orderGoods.amount}));
- const myOrder = await dispatch(actionPromise('myOrder', gql(`mutation myOrder($order:OrderInput) {
- OrderUpsert(order:$order) {
- _id createdAt total
- }
- }`, {order: {orderGoods: order}})));
- if(myOrder) {
- dispatch(actionCartClear());
- }
- }
- const orders = () =>
- gql(`query myOrders {
- OrderFind(query:"[{}]"){
- _id total orderGoods{
- price count total good{
- _id name images{
- url
- }
- }
- }
- }
- }`, {})
- const actionOrders = () =>
- actionPromise('myOrders', orders())
- const actionLogin = (login, password) =>
- actionPromise('login', gql(`query log($login:String, $password:String) {
- login(login:$login, password:$password)
- }`, {login, password}));
- const actionRegister = (login, password) =>
- actionPromise('register', gql(`mutation register($login:String, $password:String) {
- UserUpsert(user:{login:$login, password:$password}) {
- _id login createdAt
- }
- }`, {login, password}));
- const actionFullLogin = (login, password) =>
- async dispatch => {
- const token = await dispatch(actionLogin(login, password))
- if (token){
- dispatch(actionAuthLogin(token));
- goToMainPage()
- }
- }
- const actionFullRegister = (login, password) =>
- async dispatch => {
- const user = await dispatch(actionRegister(login, password))
- if(user) {
- dispatch(actionFullLogin(login, password))
- }
- }
- const actionFullOrders = () =>
- async dispatch => {
- await dispatch(actionOrders());
- if(Object.keys(store.getState().auth).length === 0) {
- goToMainPage()
- }
- }
- function Password(parent, open) {
- const input = document.createElement('input');
- input.id = 'password'
- input.type = 'password';
- parent.appendChild(input);
- const button = document.createElement('button');
- button.type = 'button';
- button.textContent = 'показать';
- parent.appendChild(button);
- button.addEventListener('click', () => {
- this.setOpen(open !== true);
- });
- this.setValue = newValue => input.value = newValue;
- this.getValue = () => input.value;
- this.setOpen = openUpdate => {
- open = openUpdate;
- if(typeof this.onOpenChange === 'function') {
- this.onOpenChange(openUpdate);
- }
- button.textContent = (openUpdate) ? 'показать' : 'скрыть';
- input.type = (openUpdate) ? 'password' : 'text';
- }
- this.getOpen = () => open;
- input.addEventListener('input', event => {
- if (typeof this.onChange === 'function'){
- this.onChange(event.target.value);
- }
- });
- }
- function LoginForm(parent) {
- const createDivider = () => parent.appendChild(document.createElement('br'));
- const input = document.createElement('input');
- input.id = 'login';
- input.type = 'text';
- parent.appendChild(input);
- const button = document.createElement('button');
- button.type = 'button';
- button.textContent = 'Логин';
- button.disabled = true;
- input.addEventListener('input', event => {
- if (typeof this.onChange === 'function'){
- this.onChange(event.target.value);
- }
- });
- button.addEventListener('click', event => {
- if (typeof this.onLogin === 'function') {
- this.onLogin(event.target);
- }
- });
- this.getLogin = () => input.value;
- createDivider();
- const password = new Password(parent, true);
- const getPassword = () => password.getValue();
- createDivider();
- parent.appendChild(button);
- const isDisabled = () => button.disabled = (!(getPassword() !== '' && this.getLogin() !== ''));
- password.onChange = () => isDisabled();
- this.onChange = () => isDisabled();
- this.setButtonText = newText => button.textContent = newText;
- }
- store.subscribe(() => {
- const rootCats = store.getState().promise.rootCategories?.payload
- if (rootCats){
- aside.innerHTML = ''
- for (let {_id, name} of rootCats){
- const a = document.createElement('a')
- a.innerText = name
- a.href = `#/category/${_id}`
- aside.append(a)
- }
- }
- })
- store.subscribe(() => {
- const catById = store.getState().promise.catById?.payload
- const [,route] = location.hash.split('/')
- if (catById && route === 'category'){
- const {name, goods, parent, subCategories} = catById;
- main.innerHTML = `<h1>${name}</h1>`;
- if(parent) {
- const {_id, name} = parent;
- const breadcrumbs = document.createElement('a');
- breadcrumbs.innerText = name;
- breadcrumbs.href = `#/category/${_id}`;
- main.prepend(breadcrumbs);
- }
- if(subCategories) {
- const listSubCategories = document.createElement('ul');
- for (let {_id, name} of subCategories) {
- listSubCategories.innerHTML += `
- <li>
- <a href="#/category/${_id}">${name}</a>
- </li>
- `;
- }
- main.append(listSubCategories);
- }
- for (let good of goods){
- const {_id, name, price, images} = good
- const a = document.createElement('a')
- a.classList.add('card')
- a.innerHTML = `
- <div>
- <img class="card__image" alt="${name}" src="${backendURL + images[0]?.url}" />
- <h2>${name}</h2>
- <strong>${price}</strong>
- </div>
- `
- a.href = `#/good/${_id}`
- const button = document.createElement('button')
- button.type = 'button';
- button.innerText = 'добавить в корзину'
- button.onclick = () => {
- store.dispatch(actionCartAdd(good))
- }
- main.append(a)
- main.append(button)
- }
- }
- })
- store.subscribe(() => {
- const goodById = store.getState().promise.goodById?.payload
- const [,route] = location.hash.split('/')
- if (goodById && route === 'good'){
- const {name, description, price, images} = goodById
- main.innerHTML = `
- <div>
- <h1>${name}</h1>
- <p>${description}</p>
- <strong>${price}</strong>
- </div>
- `;
- const button = document.createElement('button')
- button.type = 'button';
- button.innerText = 'добавить в корзину'
- button.onclick = () => {
- store.dispatch(actionCartAdd(goodById))
- }
- main.append(button);
- let imageGroup = document.createElement('div');
- imageGroup.classList.add('good-images')
- for (const img in images) {
- imageGroup.innerHTML += `
- <div>
- <img class="good-images__element" src="${backendURL + images[img]?.url}" alt="${name} photo-${img}">
- </div>
- `
- }
- main.append(imageGroup)
- }
- })
- const drawCart = () => {
- const cart = store.getState().cart;
- const [,route] = location.hash.split('/');
- if (cart && route === 'cart'){
- if(Object.keys(cart).length === 0) {
- main.innerHTML = '<h1>Корзина пустая</h1>';
- } else {
- main.innerHTML = '';
- const cartBlock = document.createElement('div');
- cartBlock.classList.add('cart');
- const totalAmountByPosition = [];
- for (const good of Object.values(cart)) {
- const {_id, name, price, images} = good.good;
- totalAmountByPosition.push(price * good.amount);
- const cartElement = document.createElement('div');
- cartElement.classList.add('cart__element')
- cartElement.innerHTML = `
- <figure class="cart__figure">
- <a class="cart__image-link" href="#/good/${_id}">
- <img class="cart__image" src="${backendURL + images[0]?.url}" alt="${name}">
- </a>
- <figcaption class="cart__caption">
- <a href="#/good/${_id}" class="cart__name">${name}</a>
- <strong class="cart__price">Цена - ${price}</strong>
- <p class="cart__amount">Количество - ${good.amount} шт.</p>
- <p class="cart__amount-total">Итого по позиции - ${price * good.amount}</p>
- </figcaption>
- </figure>
- `;
- const cartQuantity = document.createElement('fieldset');
- cartQuantity.classList.add('cart__quantity');
- const cartQuantityAmount = document.createElement('span');
- const incDecButton = (text, classStr, incDec) => {
- const button = document.createElement('button');
- button.type = 'button';
- button.id = `${(incDec ? 'decrease' : 'increase')}`
- button.innerText = text;
- button.classList.add('cart__quantity-button', `cart__quantity-button--${classStr}`);
- button.onclick = () => {
- store.dispatch(actionCartAdd(good.good, (incDec ? -1 : +1)));
- if(cart[_id].amount > 1 && button.id === 'decrease') {
- button.disabled = false
- }
- }
- if(cart[_id].amount === 1 && button.id === 'decrease') {
- button.disabled = true;
- }
- return button;
- }
- cartQuantityAmount.classList.add('cart__quantity-amount');
- cartQuantityAmount.innerText = good.amount;
- cartElement.append(cartQuantity);
- cartQuantity.append(incDecButton('-', 'decrease', true));
- cartQuantity.append(cartQuantityAmount);
- cartQuantity.append(incDecButton('+', 'increase', false));
- const deleteButton = document.createElement('button');
- deleteButton.type = 'button'
- deleteButton.innerText = 'Удалить товар'
- deleteButton.classList.add('cart__delete-button')
- deleteButton.onclick = () => {
- store.dispatch(actionCartDelete(good.good))
- }
- cartElement.append(deleteButton);
- cartBlock.append(cartElement);
- }
- main.append(cartBlock)
- const totalPrice = document.createElement('p');
- totalPrice.innerHTML = `Итого - <b>${totalAmountByPosition.reduce((a, b) => a + b, 0)}</b>`;
- main.append(totalPrice);
- const makeOrderButton = document.createElement('button');
- makeOrderButton.type = 'button';
- makeOrderButton.innerText = 'Оформить заказ';
- makeOrderButton.classList.add('cart-button');
- makeOrderButton.onclick = () => {
- store.dispatch(actionOrder());
- }
- main.append(makeOrderButton)
- }
- }
- }
- store.subscribe(drawCart);
- store.subscribe(() => {
- const orders = store.getState().promise.myOrders?.payload;
- const [,route] = location.hash.split('/')
- if(orders && route === 'orderhistory') {
- main.innerHTML = '<h1>Мои заказы</h1>';
- const orderCartGroup = document.createElement('div');
- orderCartGroup.classList.add('order-cart-group');
- for (const [index, value] of Object.entries(orders)) {
- const orderCartGroupElement = document.createElement('div');
- orderCartGroupElement.classList.add('order-cart-group__element')
- orderCartGroupElement.innerHTML = `<h2>Заказ №${+index+1} (ID заказа - ${value._id})</h2>`;
- const orderCartElements = document.createElement('div');
- orderCartElements.classList.add('order-cart__elements');
- const totalAll = document.createElement('p');
- totalAll.innerHTML = `<p>Итого - <b>${value.total}</b></p>`;
- for (const goodElement of Object.values(value.orderGoods)) {
- const {price, count, total, good} = goodElement;
- const orderCartElement = document.createElement('div');
- orderCartElement.classList.add('order-cart__element');
- orderCartElement.innerHTML = `
- <figure class="order-cart__figure">
- <a class="order-cart__image-link"
- href="#/good/${good._id}"
- >
- <img class="order-cart__image"
- src="${backendURL + good.images[0]?.url}"
- alt="${good.name}"
- >
- </a>
- <figcaption class="order-cart__caption">
- <a class="order-cart__headline-link"
- href="#/good/${good._id}"
- >
- <h3 class="order-cart__headline">${good.name}</h3>
- </a>
-
- <p>Цена - ${price}</p>
- <p>Количество - ${count}</p>
- <p>Итого по позиции - ${total}</p>
- </figcaption>
- </figure>
- `;
- orderCartElement.classList.add('order-cart__element');
- orderCartElements.append(orderCartElement)
- }
- orderCartGroupElement.append(orderCartElements);
- orderCartGroupElement.append(totalAll);
- orderCartGroup.append(orderCartGroupElement)
- }
- main.append(orderCartGroup)
- }
- })
- const drawUserName = () => {
- const buttonLogout = '<button onclick="store.dispatch(actionAuthLogout())" type="button">Выйти</button>';
- const buttonLogin = '<a href="#/login/">Войти</a>';
- const buttonRegister = '<a href="#/register/">Регистрация</a>';
- authSection.innerHTML = store.getState().auth.token ? `Пользователь - <a href="#/orderhistory">${store.getState().auth.payload.sub.login}</a><div>${buttonLogout}</div>` :`Пользователь - <i>anon</i> <div>${buttonLogin} ${buttonRegister}</div>`;
- }
- drawUserName() //работаем безусловно при перезагрузке страницы
- store.subscribe(drawUserName) //а так же при обновлении redux
- // честно стырил отсюда - https://gist.github.com/realmyst/1262561?permalink_comment_id=2299442#gistcomment-2299442
- const declOfNum = (n, titles) => titles[(n % 10 === 1 && n % 100 !== 11) ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2];
- const drawCardAmount = () => {
- if(store.getState().cart && store.getState().auth.token) {
- let totalAmount = Object.values(store.getState().cart).reduce((a, b) => a + b.amount, 0);
- cartIcon.innerHTML = `<a href="#/cart">Корзина</a> - <b>${totalAmount}</b> ${declOfNum(totalAmount, ['товар', 'товара', 'товаров'])}`;
- }
- }
- drawCardAmount()
- store.subscribe(drawCardAmount)
- store.dispatch(actionAuthLogin(localStorage.authToken))
- store.dispatch(actionRootCategories())
- //#/category/АЙДИШНИК
- //#/good/АЙДИШНИК
- window.onhashchange = () => {
- const [,route, _id] = location.hash.split('/')
- const routes = {
- category() {
- store.dispatch(actionCategoryById(_id))
- },
- good(){
- store.dispatch(actionGoodById(_id))
- },
- login(){
- main.innerHTML = '';
- const loginForm = new LoginForm(main);
- loginForm.onLogin = () => store.dispatch(actionFullLogin(login.value, password.value))
- },
- register(){
- main.innerHTML = '';
- const registerForm = new LoginForm(main);
- registerForm.setButtonText('Регистрация');
- registerForm.onLogin = () => store.dispatch(actionFullRegister(login.value, password.value));
- },
- cart(){
- drawCart();
- },
- orderhistory(){
- store.dispatch(actionFullOrders())
- }
- }
- if (route in routes){
- routes[route]()
- }
- }
- window.onhashchange()
|