├── 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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------