-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added task Учебный проект: Карточка товара
- Loading branch information
Showing
6 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`, а это потребуется в дальнейшем. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export default class ProductCard { | ||
constructor(product) { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |