├── img └── favicon.png ├── components ├── Spinner │ ├── Spinner.css │ ├── Spinner.js │ └── img │ │ └── spinner.svg ├── Header │ ├── Header.css │ └── Header.js ├── Error │ ├── Error.css │ └── Error.js ├── Shopping │ ├── Shopping.css │ ├── img │ │ └── close.svg │ └── Shopping.js └── Products │ ├── Products.css │ └── Products.js ├── constants └── root.js ├── index.js ├── utils └── localStorageUtil.js ├── css └── index.css ├── index.html └── server └── catalog.json /img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letscode-dev/js-guitar-shop/HEAD/img/favicon.png -------------------------------------------------------------------------------- /components/Spinner/Spinner.css: -------------------------------------------------------------------------------- 1 | .spinner-container { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | display: flex; 6 | width: 100%; 7 | height: 100vh; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | .spinner__img { 12 | width: 200px; 13 | height: 200px; 14 | } 15 | -------------------------------------------------------------------------------- /constants/root.js: -------------------------------------------------------------------------------- 1 | const ROOT_PRODUCTS = document.getElementById('products'); 2 | const ROOT_HEADER = document.getElementById('header'); 3 | const ROOT_SHOPPING = document.getElementById('shopping'); 4 | const ROOT_SPINNER = document.getElementById('spinner'); 5 | const ROOT_ERROR = document.getElementById('error'); 6 | -------------------------------------------------------------------------------- /components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .header-container { 2 | display: flex; 3 | height: 50px; 4 | padding: var(--spacing-small); 5 | background-color: var(--color-medium); 6 | } 7 | .header-counter { 8 | margin-left: auto; 9 | padding-right: var(--spacing-small); 10 | font-weight: bold; 11 | cursor: pointer; 12 | } 13 | -------------------------------------------------------------------------------- /components/Error/Error.css: -------------------------------------------------------------------------------- 1 | .error-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100%; 6 | height: 100vh; 7 | } 8 | .error-message { 9 | min-width: 400px; 10 | padding: 20px 40px; 11 | border-left: 7px solid var(--color-error-dark); 12 | text-align: center; 13 | background-color: var(--color-error-light); 14 | color: var(--color-white); 15 | box-shadow: var(--box-shadow); 16 | } 17 | -------------------------------------------------------------------------------- /components/Error/Error.js: -------------------------------------------------------------------------------- 1 | class Error { 2 | render() { 3 | const html = ` 4 |
5 |
6 |

Нет доступа!

7 |

Попробуйти зайти позже

8 |
9 |
10 | `; 11 | 12 | ROOT_ERROR.innerHTML = html; 13 | } 14 | } 15 | 16 | const errorPage = new Error(); 17 | -------------------------------------------------------------------------------- /components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | class Spinner { 2 | handleClear() { 3 | ROOT_SPINNER.innerHTML = ''; 4 | } 5 | 6 | render() { 7 | const html = ` 8 |
9 | 10 |
11 | `; 12 | 13 | ROOT_SPINNER.innerHTML = html; 14 | } 15 | } 16 | 17 | const spinnerPage = new Spinner(); 18 | -------------------------------------------------------------------------------- /components/Header/Header.js: -------------------------------------------------------------------------------- 1 | class Header { 2 | handlerOpenShoppingPage() { 3 | shoppingPage.render(); 4 | } 5 | 6 | render(count) { 7 | const html = ` 8 |
9 |
10 | 🔥 ${count} 11 |
12 |
13 | `; 14 | 15 | ROOT_HEADER.innerHTML = html; 16 | } 17 | }; 18 | 19 | const headerPage = new Header(); 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function render() { 2 | const productsStore = localStorageUtil.getProducts(); 3 | 4 | headerPage.render(productsStore.length); 5 | productsPage.render(); 6 | } 7 | 8 | spinnerPage.render(); 9 | 10 | let CATALOG = []; 11 | 12 | // https://api.myjson.com/bins/jvsbu 13 | fetch('server/catalog.json') 14 | .then(res => res.json()) 15 | .then(body => { 16 | CATALOG = body; 17 | 18 | setTimeout(() => { 19 | spinnerPage.handleClear(); 20 | render(); 21 | }, 1000); 22 | }) 23 | .catch(() => { 24 | spinnerPage.handleClear(); 25 | errorPage.render(); 26 | }) 27 | -------------------------------------------------------------------------------- /components/Spinner/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Shopping/Shopping.css: -------------------------------------------------------------------------------- 1 | .shopping-container { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | width: 97%; 6 | height: 97vh; 7 | margin: auto; 8 | padding: var(--spacing-large); 9 | transform: translate(-50%, -50%); 10 | background-color: var(--color-white); 11 | box-shadow: var(--box-shadow); 12 | } 13 | .shopping-element__name { 14 | padding: var(--spacing-medium); 15 | font-weight: bold; 16 | } 17 | .shopping-element__price { 18 | padding: var(--spacing-medium); 19 | color: var(--color-unaccent); 20 | } 21 | .shopping__close { 22 | position: absolute; 23 | top: 20px; 24 | right: 20px; 25 | width: 35px; 26 | height: 35px; 27 | background-image: url(img/close.svg); 28 | background-repeat: no-repeat; 29 | background-position: center center; 30 | background-size: contain; 31 | cursor: pointer; 32 | } 33 | -------------------------------------------------------------------------------- /utils/localStorageUtil.js: -------------------------------------------------------------------------------- 1 | class LocalStorageUtil { 2 | constructor() { 3 | this.keyName = 'products'; 4 | } 5 | getProducts() { 6 | const productsLocalStorage = localStorage.getItem(this.keyName); 7 | if (productsLocalStorage !== null) { 8 | return JSON.parse(productsLocalStorage); 9 | } 10 | return []; 11 | } 12 | putProducts(id) { 13 | let products = this.getProducts(); 14 | let pushProduct = false; 15 | const index = products.indexOf(id); 16 | 17 | if (index === -1) { 18 | products.push(id); 19 | pushProduct = true; 20 | } else { 21 | products.splice(index, 1); 22 | } 23 | 24 | localStorage.setItem(this.keyName, JSON.stringify(products)); 25 | 26 | return { pushProduct, products }; 27 | } 28 | }; 29 | 30 | const localStorageUtil = new LocalStorageUtil(); 31 | -------------------------------------------------------------------------------- /components/Shopping/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-light: #f8f9fa; 3 | --color-medium: #e8e8e8; 4 | --color-dark: #a1a1a1; 5 | --color-unaccent: #808080; 6 | --color-white: #fff; 7 | --color-error-light: #f26b6b; 8 | --color-error-dark: #d65c5c; 9 | 10 | --spacing-small: 10px; 11 | --spacing-medium: 15px; 12 | --spacing-large: 30px; 13 | 14 | --border-radius: 3px; 15 | --linear-gradient: linear-gradient(to right, #e2f87c, #d6f567, #c8f151, #b9ee38, #a8eb12); 16 | --box-shadow: 17 | 0 16px 24px 2px rgba(33,37,41,.02), 18 | 0 6px 30px 5px rgba(33,37,41,.04), 19 | 0 8px 10px -5px rgba(33,37,41,.1); 20 | } 21 | 22 | * { 23 | box-sizing: border-box; 24 | } 25 | body { 26 | margin: 0; 27 | padding: 0; 28 | font-family: sans-serif; 29 | line-height: 1.7; 30 | background: var(--color-light); 31 | } 32 | 33 | ::-webkit-scrollbar { 34 | width: 8px; 35 | } 36 | ::-webkit-scrollbar-thumb { 37 | background-color: var(--color-dark); 38 | border-radius: 5px; 39 | } 40 | ::-webkit-scrollbar-track { 41 | background-color: var(--color-white); 42 | } 43 | -------------------------------------------------------------------------------- /components/Products/Products.css: -------------------------------------------------------------------------------- 1 | .products-container { 2 | display: grid; 3 | grid-gap: 45px; 4 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 5 | width: 80%; 6 | max-width: 1200px; 7 | margin: var(--spacing-large) auto; 8 | } 9 | .products-element { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: flex-end; 13 | padding: 25px 20px; 14 | border-radius: var(--border-radius); 15 | background-color: var(--color-white); 16 | box-shadow: var(--box-shadow); 17 | } 18 | .products-element__name { 19 | margin-bottom: auto; 20 | text-transform: uppercase; 21 | font-weight: bold; 22 | } 23 | .products-element__img { 24 | width: 100%; 25 | height: 270px; 26 | margin-top: var(--spacing-medium); 27 | object-fit: contain; 28 | } 29 | .products-element__price { 30 | margin-top: var(--spacing-medium); 31 | color: var(--color-unaccent); 32 | } 33 | .products-element__btn { 34 | margin-top: var(--spacing-medium); 35 | padding: var(--spacing-small) var(--spacing-medium); 36 | border: 1px solid var(--color-unaccent); 37 | border-radius: var(--border-radius); 38 | cursor: pointer; 39 | outline: none; 40 | background: none; 41 | font-family: inherit; 42 | font-size: inherit; 43 | } 44 | .products-element__btn_active { 45 | border: 1px solid transparent; 46 | background-image: var(--linear-gradient); 47 | } 48 | -------------------------------------------------------------------------------- /components/Shopping/Shopping.js: -------------------------------------------------------------------------------- 1 | class Shopping { 2 | handlerClear() { 3 | ROOT_SHOPPING.innerHTML = ''; 4 | } 5 | 6 | render() { 7 | const productsStore = localStorageUtil.getProducts(); 8 | let htmlCatalog = ''; 9 | let sumCatalog = 0; 10 | 11 | CATALOG.forEach(({ id, name, price }) => { 12 | if (productsStore.indexOf(id) !== -1) { 13 | htmlCatalog += ` 14 | 15 | ⚡️ ${name} 16 | ${price.toLocaleString()} USD 17 | 18 | `; 19 | sumCatalog += price; 20 | } 21 | }); 22 | 23 | const html = ` 24 |
25 |
26 | 27 | ${htmlCatalog} 28 | 29 | 30 | 31 | 32 |
💥 Сумма:${sumCatalog.toLocaleString()} USD
33 |
34 | `; 35 | 36 | ROOT_SHOPPING.innerHTML = html; 37 | } 38 | }; 39 | 40 | const shoppingPage = new Shopping(); 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Guitar Shop 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /server/catalog.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "el1", 4 | "name": "FENDER SQUIER BULLET STRAT HT HSS BLK", 5 | "img": "https://i.ibb.co/QJfqs4K/1.jpg", 6 | "price": 13600 7 | }, 8 | { 9 | "id": "el2", 10 | "name": "FENDER SQUIER AFFINITY", 11 | "img": "https://i.ibb.co/pKrG5ZJ/2.jpg", 12 | "price": 23900 13 | }, 14 | { 15 | "id": "el3", 16 | "name": "IBANEZ GIO GRG121DX-BKF BLACK FLAT", 17 | "img": "https://i.ibb.co/NnvcYhR/3.jpg", 18 | "price": 21600 19 | }, 20 | { 21 | "id": "el4", 22 | "name": "EPIPHONE LES PAUL STUDIO LT EBONY", 23 | "img": "https://i.ibb.co/4Tg91WL/4.jpg", 24 | "price": 19900 25 | }, 26 | { 27 | "id": "el5", 28 | "name": "FENDER SQUIER BULLET TREM HSS AWT", 29 | "img": "https://i.ibb.co/MC7sy1F/5.jpg", 30 | "price": 14500 31 | }, 32 | { 33 | "id": "el6", 34 | "name": "FENDER SQUIER BULLET MUSTANG HH BLK", 35 | "img": "https://i.ibb.co/qNBWPb0/6.jpg", 36 | "price": 14600 37 | }, 38 | { 39 | "id": "el7", 40 | "name": "FENDER SQUIER BULLET STRAT HT AWT", 41 | "img": "https://i.ibb.co/WH1h2wV/7.jpg", 42 | "price": 13600 43 | }, 44 | { 45 | "id": "el8", 46 | "name": "JACKSON JS22-7 DINKY SATIN BLACK", 47 | "img": "https://i.ibb.co/2cBg9g9/8.jpg", 48 | "price": 28700 49 | }, 50 | { 51 | "id": "el9", 52 | "name": "JACKSON JS11 DINKY OLYMPIC WHITE", 53 | "img": "https://i.ibb.co/wKxFRM8/9.jpg", 54 | "price": 18100 55 | }, 56 | { 57 | "id": "el10", 58 | "name": "YAMAHA PACIFICA 012 WH", 59 | "img": "https://i.ibb.co/30qJcZX/10.jpg", 60 | "price": 15990 61 | } 62 | ] -------------------------------------------------------------------------------- /components/Products/Products.js: -------------------------------------------------------------------------------- 1 | class Products { 2 | constructor() { 3 | this.classNameActive = 'products-element__btn_active'; 4 | this.labelAdd = 'Добавить в корзину'; 5 | this.labelRemove = 'Удалить из корзины'; 6 | } 7 | 8 | handlerSetLocatStorage(element, id) { 9 | const { pushProduct, products } = localStorageUtil.putProducts(id); 10 | 11 | if (pushProduct) { 12 | element.classList.add(this.classNameActive); 13 | element.innerText = this.labelRemove; 14 | } else { 15 | element.classList.remove(this.classNameActive); 16 | element.innerText = this.labelAdd; 17 | } 18 | 19 | headerPage.render(products.length); 20 | } 21 | 22 | render() { 23 | const productsStore = localStorageUtil.getProducts(); 24 | let htmlCatalog = ''; 25 | 26 | CATALOG.forEach(({ id, name, price, img }) => { 27 | let activeClass = ''; 28 | let activeText = ''; 29 | 30 | if (productsStore.indexOf(id) === -1) { 31 | activeText = this.labelAdd; 32 | } else { 33 | activeClass = ' ' + this.classNameActive; 34 | activeText = this.labelRemove; 35 | } 36 | 37 | htmlCatalog += ` 38 |
  • 39 | ${name} 40 | 41 | 42 | ⚡️ ${price.toLocaleString()} USD 43 | 44 | 47 |
  • 48 | `; 49 | }); 50 | 51 | const html = ` 52 | 55 | `; 56 | 57 | ROOT_PRODUCTS.innerHTML = html; 58 | } 59 | }; 60 | 61 | const productsPage = new Products(); 62 | --------------------------------------------------------------------------------