123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793 |
- 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;
- }
- function jwtDecode(token) {
- try {
- let newToken = token.split('.')[1];
- return JSON.parse(atob(newToken));
- } catch (e) {
- console.error(e);
- }
- //раскодировать токен:
- //выкусить середочку
- //atob
- //JSON.parse
- //на любом этапе могут быть исключения
- }
- function getTokenFromLS() {
- const token = localStorage.getItem('auth');
- return !!token && token !== 'undefined' ? {token} : {};
- }
- function authReducer(state = getTokenFromLS(), {type, token}) {
- if (!state) {
- if (localStorage.getItem('auth')) {
- type = 'AUTH_LOGIN';
- token = localStorage.getItem('auth');
- } else {
- return {};
- }
- //проверить localStorage.authToken на наличие
- //если есть - сделать так, что бы следующий if сработал
- //если нет - вернуть {}
- }
- if (type === 'AUTH_LOGIN') {
- // token = localStorage.getItem('authToken');
- let decodeToken = jwtDecode(token);
- //взять токен из action
- //попытаться его jwtDecode
- //если удалось, то:
- //сохранить токен в localStorage
- //вернуть объект вида {токен, payload: раскодированный токен}
- return {
- token,
- payload: decodeToken,
- };
- }
- if (type === 'AUTH_LOGOUT') {
- localStorage.removeItem('auth');
- return {};
- //почистить localStorage
- //вернуть пустой объект
- }
- console.log(state);
- return state;
- }
- function combineReducers(reducers) {
- return (state = {}, action) => {
- const newState = {};
- for (const [reducerName, reducer] of Object.entries(reducers)) {
- const newSubState = reducer(state[reducerName], action);
- if (newSubState !== state[reducerName]) {
- newState[reducerName] = newSubState;
- }
- }
- if (Object.keys(newState).length !== 0) {
- return {...state, ...newState};
- } else {
- return state;
- }
- //перебрать все редьюсеры
- //запустить каждый их них
- //передать при этом в него ЕГО ВЕТВЬ общего state, и action как есть
- //получить newSubState
- //если newSubState отличается от входящего, то записать newSubState в newState
- //после цикла, если newState не пуст, то вернуть {...state, ...newState}
- //иначе вернуть state
- };
- }
- const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer});
- const store = createStore(combinedReducer);
- const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
- const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'});
- function getCartFromLS() {
- const cart = JSON.parse(localStorage.getItem('cart'));
- return !!cart ? cart : {};
- }//??
- function cartReducer(state = getCartFromLS(), {type, good = {}, count = 1}) {
- const {_id} = good;
- // const {count} = good;
- // {
- // _id1: {good, count}
- // _id2: {good, count}
- // }
- const types = {
- CART_ADD() { //как CHANGE, только если ключ раньше был, то достать из count и добавить
- //к count из action. Если не было, достать 0 и добавить к count из action
- return {
- ...state, //по аналогии с promiseReducer дописать
- [_id]: {good, count: count + (state[_id]?.count || 0)}
- };
- },
- CART_REMOVE() { //смочь скопировать объект и выкинуть ключ. как вариант через
- //деструктуризацию
- return Object.fromEntries(Object.entries(state).filter(([key, value]) => {
- return key !== _id;
- }));
- },
- CART_CHANGE() {
- return {
- ...state, //по аналогии с promiseReducer дописать
- [_id]: {good, count}
- };
- },
- CART_CLEAR() {
- return {};
- },
- };
- if (type in types) {
- return types[type]();
- }
- return state;
- }
- //понаписывать action
- //прикрутить к товару кнопку которая делает store.dispatch(actionCartAdd(good))
- // store.dispatch({type: 'CART_CHANGE', good: {_id: 'пиво', name: 'пиво'}, count: 10});
- // console.log(store.dispatch({type: 'CART_CHANGE', good: {_id: 'пиво', name: 'пиво'}, count: 10}));
- // store.dispatch({type: 'CART_CHANGE', good: {_id: 'вода', name: 'вода'}, count: 10});
- // store.dispatch({type: 'CART_ADD', good: {_id: 'вода', name: 'вода'}, count: 6});
- // store.dispatch({type: 'CART_ADD', good: {_id: 'вода', name: 'вода'}, count: 12});
- // store.dispatch({type: 'CART_CHANGE', good: {_id: 'сок', name: 'сок'}, count: 10});
- // store.dispatch({type: 'CART_REMOVE', good: {_id: 'вода', name: 'вода'}, count: 9});
- //ПЕРЕДЕЛАТЬ ОТОБРАЖЕНИЕ с поправкой на то, что теперь промисы не в корне state а в state.promise
- const actionLogin = (login, password) =>
- actionPromise('login', gql(`query find($login: String, $password: String){
- login(login:$login, password: $password)
- }`, {login: login, password: password}));
- const actionFullLogin = (login, password) =>
- async dispatch => {
- const token = await dispatch(actionLogin(login, password));
- if (token) {
- await dispatch(actionAuthLogin(token));
- await dispatch(actionMyOrders());
- }
- };
- const actionRegister = (login, password) =>
- actionPromise('register', gql(`mutation reg($login: String, $password: String){
- UserUpsert(user:{login:$login,
- password: $password,
- nick:$login}){
- _id login
- }
- }`, {login: login, password: password}));//actionPromise
- const actionFullRegister = (login, password) => //actionRegister + actionFullLogin
- async dispatch => {
- try {
- await dispatch(actionRegister(login, password));
- } catch (e) {
- return console.log(e);
- }
- await dispatch(actionFullLogin(login, password));
- };
- // + интерфейс к этому - форму логина, регистрации, может повесить это на #/login #/register
- // + #/orders показывает ваши бывшие заказы:
- // сделать actionMyOrders
- const actionCartAdd = (good, count = 1) => ({type: 'CART_ADD', good, count});
- const actionCartChange = (good, count = 1) => ({type: 'CART_CHANGE', good, count});
- const actionCartDelete = (good, count = 0) => ({type: 'CART_REMOVE', good, count});
- const actionCartClear = () => ({type: 'CART_CLEAR'});
- store.subscribe(() => console.log(store.getState()));
- //проверить:
- //поделать store.dispatch с разными action. Скопипастить токен
- //проверить перезагрузку страницы.
- 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)); // 1. {delay1000: {status: 'PENDING'}}
- try {
- let payload = await promise;
- dispatch(actionResolved(name, payload));
- return payload;
- } catch (error) {
- dispatch(actionRejected(name, error));
- }
- };
- const getGQL = url =>
- (query, variables = {}) =>
- fetch(url, {
- //метод
- method: 'POST',
- headers: {
- //заголовок content-type
- 'Content-Type': 'application/json',
- ...(localStorage.getItem('auth') ? {'Authorization': 'Bearer ' + localStorage.getItem('auth')} : {})
- },
- //body с ключами query и variables
- body: JSON.stringify({query, variables})
- })
- .then(res => res.json())
- .then(data => {
- if (data.errors && !data.data)
- throw new Error(JSON.stringify(data.errors));
- return data.data[Object.keys(data.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 description images {
- url _id good{
- _id name description images{
- _id url
- }
- }
- }
- } subCategories{
- _id name image{
- url
- }
- }
- }
- }`, {q: JSON.stringify([{_id}])}));
- const actionGoodById = (_id) =>
- actionPromise('goodById', gql(`
- query goodById ($good:String) {
- GoodFindOne(query: $good) {
- name
- description
- price
- categories {
- name
- }
- images {
- url
- }
- }
- }`, {good: JSON.stringify([{_id}])}));
- store.dispatch(actionRootCats());
- const actionOrder = () =>
- async (dispatch, getState) => {
- let {cart} = getState();
- const orderGoods = Object.entries(cart)
- .map(([_id, {...key}]) => ({good: {_id}, count: key.count}));
- let result = await dispatch(actionPromise('order', gql(`
- mutation newOrder($order:OrderInput){
- OrderUpsert(order:$order)
- { _id total orderGoods{
- price count good{
- name createdAt price images{
- url
- }
- }
- }
- }
- }
- `, {order: {orderGoods}})));
- if (result?._id) {
- dispatch(actionCartClear());
- }
- };
- actionMyOrders = () =>
- actionPromise('myOrders', gql(
- `query o{
- OrderFind(query:"[{}]"){
- _id total orderGoods{
- price count total
- good{
- createdAt name price images{
- url text
- }
- }
- }
- }
- }`));
- 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);
- }
- }
- });
- window.onhashchange = () => {
- const [, route, _id] = location.hash.split('/');
- const routes = {
- category() {
- store.dispatch(actionCatById(_id));
- console.log('КАТЕГОРИИСТРАНИЦА');
- },
- good() { //задиспатчить actionGoodById
- store.dispatch(actionGoodById(_id));
- console.log('ТОВАРОСТРАНИЦА');
- },
- login() {
- console.log('LOGIN');
- main.innerHTML = '';
- const token = localStorage.getItem('auth');
- if (!token || token === 'undefined') {
- const h2Greeting = document.createElement('h2');
- h2Greeting.textContent = 'Вход в личный кабинет';
- const loginInput = document.createElement('input');
- loginInput.type = 'login';
- loginInput.className = 'loginInput';
- loginInput.placeholder = 'Логин';
- const pswInput = document.createElement('input');
- pswInput.type = 'password';
- pswInput.className = 'pswInput';
- pswInput.placeholder = 'Пароль';
- const buttonSend = document.createElement('button');
- buttonSend.className = 'buttonSend';
- buttonSend.textContent = 'Войти';
- buttonSend.onclick = () => {
- if (loginInput.value !== '' && pswInput.value !== '') {
- loginInput.style.borderColor = '#ccc';
- pswInput.style.borderColor = '#ccc';
- store.dispatch(actionFullLogin(loginInput.value, pswInput.value));
- console.log('нажала на логин');
- location.href = `#/dashboard/${_id}`;
- } else {
- loginInput.style.borderColor = 'red';
- pswInput.style.borderColor = 'red';
- loginInput.placeholder = 'Введите логин!';
- pswInput.placeholder = 'Введите пароль!';
- }
- };
- main.appendChild(h2Greeting);
- main.appendChild(loginInput);
- main.appendChild(pswInput);
- main.appendChild(buttonSend);
- const divQuestion = document.createElement('div');
- divQuestion.textContent = 'Вы еще не зарегистрированы?';
- divQuestion.className = 'divQuestion';
- const a = document.createElement('a');
- a.className = 'link';
- a.href = `#/register/${_id}`;
- a.textContent = 'Регистрация';
- main.appendChild(divQuestion);
- main.appendChild(a);
- console.log('Задиспатчила логин и пароль по клику');
- } else {
- location.href = `#/dashboard/${_id}`; //??
- console.log('перехожу в доску заказов, птмш уже авторизована');
- }
- },
- register() {
- console.log('я в форме регистрации');
- main.innerHTML = '';
- const h2Greeting = document.createElement('h2');
- h2Greeting.textContent = 'Регистрация нового пользователя';
- const loginInputForName = document.createElement('input');
- loginInputForName.type = 'text';
- loginInputForName.className = 'loginInput';
- loginInputForName.placeholder = 'Ваше имя';
- const loginInputForSurname = document.createElement('input');
- loginInputForSurname.type = 'text';
- loginInputForSurname.className = 'loginInput';
- loginInputForSurname.placeholder = 'Ваша фамилия';
- const loginInput = document.createElement('input');
- loginInput.type = 'text';
- loginInput.className = 'loginInput';
- loginInput.placeholder = 'Логин*';
- const pswInput = document.createElement('input');
- pswInput.type = 'password';
- pswInput.className = 'pswInput';
- pswInput.placeholder = 'Пароль*';
- const buttonSend = document.createElement('button');
- buttonSend.className = 'buttonSend';
- buttonSend.textContent = 'Зарегистрироваться';
- main.appendChild(h2Greeting);
- main.appendChild(loginInputForName);
- main.appendChild(loginInputForSurname);
- main.appendChild(loginInput);
- main.appendChild(pswInput);
- main.appendChild(buttonSend);
- buttonSend.onclick = function register(e) {
- store.dispatch(actionFullRegister(loginInput.value, pswInput.value));
- const a = document.createElement('a');
- a.href = `#/login/${_id}`;
- a.textContent = 'Войти в личный кабинет';
- main.appendChild(a);
- };
- console.log('передаю данные на регистрацию');
- },
- cart() {
- if (Object.keys(store.getState().cart).length !== 0) {
- main.innerHTML = '';
- for (const key in store.getState().cart) {
- const {good} = store.getState().cart[key];
- let {count} = store.getState().cart[key];
- let {name, price, images} = good;
- const headerName = document.createElement('h2');
- headerName.innerHTML = name;
- main.appendChild(headerName);
- const img = document.createElement('img');
- img.src = `${backURL}/${images[0].url}`;
- main.appendChild(img);
- let currentPrice = price * count;
- const divPrice = document.createElement('div');
- divPrice.innerHTML = `${currentPrice}грн`;
- main.appendChild(divPrice);
- const wrapperForCounter = document.createElement('div');
- wrapperForCounter.className = 'wrapperForCounter';
- const inputChangeNumber = document.createElement('input');
- inputChangeNumber.className = 'inputChangeNumber';
- inputChangeNumber.type = 'number';
- inputChangeNumber.value = count;
- inputChangeNumber.onclick = () => {
- const inputValue = inputChangeNumber.value;
- divPrice.innerHTML = `${price * +inputValue}грн`;
- store.dispatch(actionCartChange(good, +inputValue));
- };
- wrapperForCounter.appendChild(inputChangeNumber);
- const buttonDeleteGood = document.createElement('input');
- buttonDeleteGood.type = 'button';
- buttonDeleteGood.className = 'buttonDeleteGood';
- buttonDeleteGood.value = 'х';
- buttonDeleteGood.onclick = () => {
- store.dispatch(actionCartDelete(good, inputChangeNumber.value));
- window.location.reload()
- };
- wrapperForCounter.appendChild(buttonDeleteGood);
- main.appendChild(wrapperForCounter);
- }
- const buttonSend = document.createElement('button');
- buttonSend.className = 'buttonSend';
- buttonSend.textContent = 'Оформить заказ';
- main.appendChild(buttonSend);
- buttonSend.onclick = () => {
- store.dispatch(actionOrder(_id));
- location.href = `#/dashboard/${_id}`;
- };
- console.log('СТРАНИЦА КОРЗИНЫ');
- } else {
- main.innerHTML = 'Ваша корзина пуста :(';
- }
- },
- dashboard() {
- store.dispatch(actionMyOrders());
- main.innerHTML = '';
- const wrapperForPrivate = document.createElement('div');
- wrapperForPrivate.className = 'wrapperForPrivate';
- const divNameofPage = document.createElement('h2');
- divNameofPage.className = 'divNameofPage';
- divNameofPage.textContent = 'Личный кабинет';
- wrapperForPrivate.appendChild(divNameofPage);
- const buttonLogOff = document.createElement('button');
- buttonLogOff.className = 'buttonLogOff';
- buttonLogOff.textContent = 'Выход';
- wrapperForPrivate.appendChild(buttonLogOff);
- buttonLogOff.onclick = () => {
- store.dispatch(actionAuthLogout());
- main.innerHTML = 'Вы вышли с личного кабинета! До новых встреч :)'; //??
- };
- main.appendChild(wrapperForPrivate);
- const headerOrders = document.createElement('div');
- headerOrders.style.fontSize = '20px';
- headerOrders.textContent = 'История заказов';
- main.appendChild(headerOrders);
- console.log('СТОРЕ ДИСПАТЧ ПРОЧИТАТЬ БЫВШИЕ ЗАКАЗЫ');
- }
- };
- if (route in routes) {
- routes[route]();
- }
- };
- window.onhashchange();
- store.subscribe(() => {
- const [, route, _id] = location.hash.split('/');
- console.log(route);
- if (route === 'dashboard') {
- console.log('ДОСКА ЗАКАЗОВ');
- const auth = store.getState().auth;
- if (Object.keys(auth).length !== 0) {
- localStorage.setItem('auth', auth.token);
- const myOrders = store.getState().promise?.myOrders;
- if (myOrders?.payload) {
- myOrders?.payload.filter(order => {
- const {total, orderGoods} = order;
- if (total !== null) {
- const divDashboardOrders = document.createElement('h2');
- divDashboardOrders.className = 'divDashboardOrders';
- main.appendChild(divDashboardOrders);
- const divDescriptionOrder = document.createElement('div');
- divDescriptionOrder.className = 'divDescriptionOrder';
- const img = document.createElement('img');
- img.src = `${backURL}/${orderGoods[0].good.images[0].url}`;
- img.className = 'imgGood';
- divDashboardOrders.appendChild(img);
- const divName = document.createElement('div');
- divName.className = 'divName';
- divName.textContent = `${orderGoods[0].good.name}`;
- divDescriptionOrder.appendChild(divName);
- const wrapperForCharacteristicGood = document.createElement('div');
- wrapperForCharacteristicGood.className = 'wrapperForCharacteristicGood';
- const quantityDiv = document.createElement('div');
- quantityDiv.textContent = `${orderGoods[0]['count']}шт`;
- wrapperForCharacteristicGood.appendChild(quantityDiv);
- const totalDiv = document.createElement('div');
- totalDiv.textContent = `${order.total} грн`;
- totalDiv.className = 'totalDiv';
- wrapperForCharacteristicGood.appendChild(totalDiv);
- divDashboardOrders.appendChild(divDescriptionOrder);
- divDescriptionOrder.appendChild(wrapperForCharacteristicGood);
- }
- });
- }
- }
- }
- });
- store.subscribe(() => {
- const [, , _id] = location.hash.split('/');
- header.innerHTML = '';
- const namCat = document.createElement('div');
- namCat.className = 'nameGategory';
- namCat.textContent = 'Категории';
- const enter = document.createElement('a');
- enter.className = 'enterToPrivate';
- enter.textContent = 'Вход в кабинет';
- enter.href = `#/login/${_id}`;
- header.appendChild(namCat);
- header.appendChild(enter);
- }
- );
- store.subscribe(() => {
- const [, , _id] = location.hash.split('/');
- let wrapperForCart = document.createElement('div');
- wrapperForCart.className = 'wrapperForCart';
- const link = document.createElement('a');
- link.className = 'cart';
- header.appendChild(wrapperForCart);
- wrapperForCart.innerHTML = `
- <a href="#/cart/${_id}">Корзина</a>
- <img src="cart.png" class="cartImg">`;
- const cartArr = Object.values(store.getState().cart);
- const counter = cartArr.length === 0 ? 0 : cartArr.reduce((accum, {count}) => accum + count, 0);
- wrapperForCart.innerHTML = `<a href="#/cart/${_id}">Корзина ${counter}</a>
- <img src="cart.png" class="cartImg">`;
- }
- );
- store.subscribe(() => {
- const {catById} = store.getState().promise;
- const [, route, _id] = location.hash.split('/');
- if (catById?.payload && route === 'category') {
- const {name} = catById.payload;
- main.innerHTML = `<h1>${name}</h1> `;
- if (catById.payload.subCategories) {
- console.log('тут подкатегории');
- for (let good of catById.payload.subCategories) {
- main.innerHTML += `<h2>${good.name}</h2>`;
- main.innerHTML += `<a href="#/category/${good._id}">Подробнее</a>`;
- }
- }
- if (catById.payload.goods) {
- for (const good of catById.payload.goods) {
- const {_id, name, price, images} = good;
- const card = document.createElement('div');
- card.className = 'card';
- const link = document.createElement('a');
- card.innerHTML = `<h2>${name}</h2>
- <img src="${backURL}/${images[0].url}" />
- <strong>${price} грн</strong>
- <a href="#/good/${_id}">Подробнее</a>
- `;
- const buttonBuy = document.createElement('button');
- buttonBuy.className = 'buttonBuy';
- buttonBuy.textContent = 'Купить';
- buttonBuy.onclick = () => {
- store.dispatch(actionCartAdd(good, 1));
- };
- card.appendChild(buttonBuy);
- aside.append(link);
- main.append(card);
- }
- }
- }
- }
- );
- store.subscribe(() => {
- const {goodById} = store.getState().promise;
- const [, route, _id] = location.hash.split('/');
- if (goodById?.payload && route === 'good') {
- console.log('я на странице описания товара');
- if (location.hash.indexOf(`#/good/${_id}`) !== -1) {
- main.innerHTML = '';
- const {name, price, images, description} = goodById.payload;
- const card = document.createElement('div');
- card.innerHTML = `<h2>${name}</h2>
- <img src="${backURL}/${images[0].url}" />
- <strong>${price}</strong>
- <div>${description}</div>
- `;
- main.append(card);
- }
- }
- }
- );
- store.subscribe(() => {
- localStorage.setItem('cart', JSON.stringify(store.getState().cart));
- });
|