Skip to content

Commit

Permalink
Added task Учебный проект: Карточка товара
Browse files Browse the repository at this point in the history
  • Loading branch information
jsru-1 committed Sep 4, 2024
1 parent 93e054a commit b51bd40
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 0 deletions.
96 changes: 96 additions & 0 deletions 6-module/2-task/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Учебный проект: Карточка товара

Создайте класс `ProductCard`, описывающий компонент "Карточка товара".

В качестве аргумента в конструктор класса передаётся объект, описывающий товар:

```js
let product = {
name: "Laab kai chicken salad", // название товара
price: 10, // цена товара
category: "salads", // категория, к которой он относится, нам это понадобится чуть позже
image: "laab_kai_chicken_salad.png", // название картинки товара
id: "laab-kai-chicken-salad" // уникальный идентификатор товара, нужен для добавления товара в корзину
}

let productCard = new ProductCard(product);
```

После этого в `productCard.elem` должен быть доступен DOM-элемент с карточкой товара.

Вот его вид:

```html
<div class="card">
<div class="card__top">
<img src="/assets/images/products/...значение product.image..." class="card__image" alt="product">
<span class="card__price">€<!--значение product.price--></span>
</div>
<div class="card__body">
<div class="card__title"><!--значение product.name--></div>
<button type="button" class="card__button">
<img src="/assets/images/icons/plus-icon.svg" alt="icon">
</button>
</div>
</div>
```

На вёрстку можно посмотреть в файле `static.html`, а пример использования компонента `ProductCard` - в файле `index.html`.

**Обращаем ваше внимание:**
- Для создания DOM-элементов, рекомендуем использовать хэлпер `createElement`, который импортируется в первой строке `index.js`: `import createElement from '../../assets/lib/create-element.js';`. Он позволяет создать, готовый елемент из вашей вёрстки, пример:
```js
import createElement from '../../assets/lib/create-element.js';

const table = createElement(`
<table class="table">
<thead>
<tr>
<th>Имя</th>
</tr>
</thead>
<tbody>
<tr>
<td>Вася</td>
</tr>
<tr>
<td>Петя</td>
</tr>
</tbody>
</table>
`)
```
- Цена представлена в объекте товара, как число (например, вот так: `10`), но отобразить её нужно, во-первых, со значком валюты в начале - ``, а во-вторых, с двумя символами после точки - `€10.00`. Чтобы получить два символа, воспользуйтесь методом числа `toFixed`, про который можно прочитать [вот в этой статье](https://learn.javascript.ru/number#okruglenie).
- Нужно дополнить путь к картинке товара. Все картинки товаров лежат в папке `/assets/images/products`. В тоже время, для каждого товара нам нужно прописать путь к конкретной картинке. Название картинки вы найдёте в свойстве `image` объекта товара. В итоге у вас должен получиться путь вида `/assets/images/products/laab_kai_chicken_salad.png`, где `laab_kai_chicken_salad.png` вы возьмёте из свойства `image`. Этот путь нужно прописать в атрибут `src` картинки `img` с CSS классом `card__image` в вёрстке.
- Созданный DOM-элемент вашей карточки, необходимо сохранить в свойство `elem` вашего класса `ProductCard`, для того чтобы его можно было использовать вот так (см. пример в `index.html`):

```js
let productCard = new ProductCard(product);
console.log(productCard.elem); // корневой HTML элемента карточки товара
```

- Cвойство `elem` не должно быть геттером(`get elem()`), который при каждом вызове создает новый DOM-елемент, так как ваш компонент могут использовать несколько раз. Поэтому допускается геттер, который просто возвращает созданный DOM-елемент, к примеру:
```js
get elem() {
return this._container;
}
```

## Событие при клике на "+"

Кроме показа карточки товара (генерации DOM-элемента), нужно генерировать событие при клике по кнопке добавления "+".

В дальнейшем этот компонент будет использоваться в списке товаров, а также будет участвовать в добавлении их в корзину.

А именно, при клике пользователя по кнопке с классом `card__button` генерировать пользовательское событие на корневом HTML элементе компонента (который хранится в свойстве `elem`), такого вида:

```js
new CustomEvent("product-add", { // имя события должно быть именно "product-add"
detail: this.product.id, // Уникальный идентификатора товара из объекта товара
bubbles: true // это событие всплывает - это понадобится в дальнейшем
}
```
Про пользовательские события можно прочитать в статье - [Генерация пользовательских событий](https://learn.javascript.ru/dispatch-events).
**(!!!)** Обращаем ваше внимание, что это событие должно **ОБЯЗАТЕЛЬНО** всплывать. Для этого не забудьте передать свойство `bubbles: true` в опциях в момент создания объекта события, как это показано выше. Если этого не сделать, событие невозможно будет отловить на элементе `body`, а это потребуется в дальнейшем.
90 changes: 90 additions & 0 deletions 6-module/2-task/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.card {
height: var(--card-height);
display: flex;
flex-direction: column;
position: relative;
transition: 0.2s all;
cursor: pointer;
}

.card:hover,
.card:hover .card__body {
background-color: #3b3a31;
}

.card:hover .card__top {
background-color: #4e4d41;
}

.card__top {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background-color: var(--color-black-middle);
}

.card__image {
max-width: calc(100% - 100px);
width: 100%;
}

.card__price {
position: absolute;
right: 0;
bottom: 0;
display: inline-block;
padding: 8px;
min-width: 72px;
text-align: center;
background-color: var(--color-pink);
color: var(--color-body);
font-family: var(--font-primary), sans-serif;
font-weight: 700;
font-size: 17px;
line-height: 1.2;
}

.card__body {
height: 70px;
background-color: var(--color-black-dark);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}

.card__title {
text-align: center;
font-weight: 500;
font-size: 21px;
font-style: italic;
line-height: 1.2;
width: 100%;
}

.card__button {
background-color: var(--color-yellow);
width: 72px;
flex: 1 0 72px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}

.card__button:hover,
.card__button:active {
background-color: var(--color-yellow-dark);
}

@media all and (max-width: 767px) {
.card {
margin-bottom: 16px;
height: auto;
}
}
30 changes: 30 additions & 0 deletions 6-module/2-task/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Бангкок Экспресс: Карточка товара</title>
<link rel="stylesheet" href="./index.css" />
<link rel="stylesheet" href="/assets/styles/common.css" />
</head>

<body>
<div id="holder" class="container_half"></div>

<script type="module">
import ProductCard from './index.js';

let card = new ProductCard({
name: "Laab kai chicken salad",
price: 10,
category: "salads",
image: "laab_kai_chicken_salad.png",
id: "laab-kai-chicken-salad"
});

holder.append(card.elem);
</script>
</body>

</html>
4 changes: 4 additions & 0 deletions 6-module/2-task/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default class ProductCard {
constructor(product) {
}
}
29 changes: 29 additions & 0 deletions 6-module/2-task/static.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Бангкок Экспресс: Карточка товара</title>
<link rel="stylesheet" href="./index.css" />
<link rel="stylesheet" href="/assets/styles/common.css" />
</head>

<body>
<div id="holder" class="container_half">
<div class="card">
<div class="card__top">
<img src="/assets/images/products/laab_kai_chicken_salad.png" class="card__image" alt="product">
<span class="card__price">€10.00</span>
</div>
<div class="card__body">
<div class="card__title">Laab kai chicken salad</div>
<button type="button" class="card__button">
<img src="/assets/images/icons/plus-icon.svg" alt="icon">
</button>
</div>
</div>
</div>
</body>

</html>
88 changes: 88 additions & 0 deletions 6-module/2-task/task.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import ProductCard from './index.js';

describe('6-module-2-task', () => {
let sut;

let product;
let clickEvent;

beforeEach(() => {
product = {
name: "Laab kai chicken salad",
price: 10,
category: "salads",
image: "laab_kai_chicken_salad.png",
id: "laab-kai-chicken-salad"
};

clickEvent = new MouseEvent('click', { bubbles: true });

sut = new ProductCard(product);

document.body.append(sut.elem);
});

afterEach(() => {
sut.elem.remove();
});

describe('отрисовка', () => {
it('свойство elem возвращает один и тот же элемент, при каждом обращении', () => {
const elementFirstCall = sut.elem;
const elementSecondCall = sut.elem;

expect(elementFirstCall).toBe(elementSecondCall);
});

it('карточка товара должна содержать картинку', () => {
let imageElement = sut.elem.querySelector('.card__image');
let actualImageSrc = imageElement.src.trim();
let expectedImageSrc = `/assets/images/products/${product.image}`;

let isCorrectSource = actualImageSrc.includes(expectedImageSrc);

expect(isCorrectSource).toBe(true);
});

it('карточка товара должна содержать цену', () => {
let priceElement = sut.elem.querySelector('.card__price');
let actualPrice = priceElement.innerHTML.trim();
let expectedPrice = '€10.00';

expect(actualPrice).toBe(expectedPrice);
});

it('карточка товара должна содержать название товара', () => {
let nameElement = sut.elem.querySelector('.card__title');
let actualName = nameElement.innerHTML.trim();
let expectedName = product.name;

expect(actualName).toBe(expectedName);
});
});

describe('генерация события добавления в корзину("product-add")', () => {
let productAddEventName;
let productAddEvent;

beforeEach(() => {
productAddEventName = 'product-add';

document.body.addEventListener(productAddEventName, (event) => {
productAddEvent = event;
}, { once: true });

let addButton = sut.elem.querySelector('.card__button');

addButton.dispatchEvent(clickEvent);
});

it('после клика по кнопке, должно быть создано событие', () => {
expect(productAddEvent instanceof CustomEvent).toBe(true);
});

it('созданное событие должно содержать в себе уникальный идентификатор товара ("id")', () => {
expect(productAddEvent.detail).toBe(product.id);
});
});
});

0 comments on commit b51bd40

Please sign in to comment.