diff --git a/5-module/3-task/README.md b/5-module/3-task/README.md new file mode 100644 index 0000000..6f8a4b7 --- /dev/null +++ b/5-module/3-task/README.md @@ -0,0 +1,53 @@ +# Учебный проект (пролог): Карусель + +![Компонент Карусель](carousel.png) + +"Карусель" - компонент интерфейса, который состоит из перемещающихся по клику на стрелки слайдов. + +Вы можете посмотреть её вёрстку (пока не "живую" карусель) в файле `index.html`. + +Рабочую карусель можно увидеть вверху страницы проекта. + +Такую карусель предстоит написать вам. Вся вёрстка и весь CSS уже готовы, их изменять не нужно. + +```js +function initCarousel() { + // ваш код... +} + +initCarousel(); // после того, как эта функция выполнится, в карусели должны начать переключаться слайды +``` + +Слайды должны перемещаться влево/вправо при клике по кнопкам вперёд/назад. + +Их CSS классы: +- `.carousel__arrow_right` - класс кнопки переключения на слайд вперёд; +- `.carousel__arrow_left` - класс кнопки переключения на слайд назад; + +Все слайды равны по ширине. В этом задании их для простоты ровно 4, и на это количество можно опираться в коде. + +### Как (технически) переключается карусель? + +Структура карусели такова, что есть внешний элемент, в котором находится "лента" из подряд идущих слайдов. Внешний элемент имеет фиксированную ширину, поэтому видна только часть ленты (один слайд). + +CSS класс элемента-ленты, в котором находятся все слайды - `.carousel__inner`. Для переключения слайда мы будем сдвигать его на ширину одного слайда. + +Допустим, что ширина одного слайда `300px`. Она может быть любой, точную ширину элемента, независимо от вёрстки, можно получить при помощи его свойства `offsetWidth`. Там хранится только число, без `px`. [Подробнее про offsetWidth](https://learn.javascript.ru/size-and-scroll#offsetwidth-height). + +Чтобы переключить на второй слайд, нужно переместить элемент с классом `.carousel__inner` на `300px` влево. Это можно сделать, изменив его свойство transform следующим образом: `elem.style.transform = 'translateX(-300px)'`. + +Мы используем отрицательное значение пикселей, т.к. нам нужно сдвинуть весь элемент влево, если бы значение было положительное, он переместился бы наоборот вправо. [Подробнее про свойство style](https://learn.javascript.ru/styles-and-classes#element-style). + + Чтобы переключить ещё на один слайд вперёд, нужно наш элемент сдвинуть ещё на `300px`, вот так: `elem.style.transform = 'translateX(-600px)` и т.д. + +### Скрываем кнопки переключения при достижении крайних слайдов + +Когда пользователь дошёл до 4-ого слайда, нужно скрыть кнопку переключения вперёд, и наоборот, когда пользователь видит первый слайд, нужно скрыть кнопку переключения назад. + +Скрывать и показывать кнопки нужно с помощью CSS свойства `display`, вот так: +- `carouselArrow.style.display = 'none'` - скрыть кнопку, +- `carouselArrow.style.display = ''` - показать кнопку, + +(Предполагается, что в переменной `carouselArrow` содержится ссылка на кнопку переключения слайдов). + + diff --git a/5-module/3-task/index.css b/5-module/3-task/index.css new file mode 100644 index 0000000..afbc2bf --- /dev/null +++ b/5-module/3-task/index.css @@ -0,0 +1,152 @@ +.carousel { + height: var(--carousel-height); + position: relative; + overflow: hidden; +} + +.carousel__caption { + position: absolute; + z-index: 2; + right: 0; + bottom: 0; + height: 70px; + background-color: var(--color-black-dark); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.carousel__price { + position: absolute; + right: 0; + bottom: 100%; + 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; +} + +.carousel__title { + text-align: center; + font-weight: 500; + font-size: 21px; + font-style: italic; + line-height: 1.2; + width: 100%; + padding: 0 20px; +} + +.carousel__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; + transition: 0.2s all; +} + +.carousel__button:hover, +.carousel__button:active { + background-color: var(--color-yellow-dark); +} + +.carousel__inner { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + position: absolute; + left: 0; + top: 0; + transition: all 1s ease; +} + +.carousel__slide { + width: 100%; + min-width: 100%; + height: 100%; + position: relative; + margin: 0; +} + +.carousel__arrow { + position: absolute; + z-index: 3; + bottom: 0; + top: 50%; + transform: translate(0, -50%); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 70px; + height: 70px; +} + +.carousel__arrow_right { + right: 0; +} + +.carousel__arrow_left { + left: 0; +} + +.carousel__arrow img, +.carousel__arrow svg { + max-width: 20px; +} + +.carousel__img { + min-width: 100%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +@media all and (max-width: 767px) { + .carousel { + padding-bottom: 57px; + } + + .carousel__caption { + left: 0; + height: 57px; + } + + .carousel__arrow { + bottom: 57px; + } + + .carousel__arrow_right img, + .carousel__arrow_right svg { + margin-top: 0; + } + + .carousel__arrow_left img, + .carousel__arrow_left svg { + margin-bottom: 0; + } +} + +@media (max-width: 992px) { + .carousel__img { + position: absolute; + top: 0; + left: 0; + transform: none; + } +} diff --git a/5-module/3-task/index.html b/5-module/3-task/index.html new file mode 100644 index 0000000..96d6558 --- /dev/null +++ b/5-module/3-task/index.html @@ -0,0 +1,77 @@ + +Бангкок Экспресс: Карусель + + + + + + +
+
+ +
+
+ + + + + + diff --git a/5-module/3-task/index.js b/5-module/3-task/index.js new file mode 100644 index 0000000..26a2fc6 --- /dev/null +++ b/5-module/3-task/index.js @@ -0,0 +1,3 @@ +function initCarousel() { + // ваш код... +} diff --git a/5-module/3-task/public/carousel.png b/5-module/3-task/public/carousel.png new file mode 100644 index 0000000..b1aa1c7 Binary files /dev/null and b/5-module/3-task/public/carousel.png differ diff --git a/5-module/3-task/task.test.js b/5-module/3-task/task.test.js new file mode 100644 index 0000000..9c6e4df --- /dev/null +++ b/5-module/3-task/task.test.js @@ -0,0 +1,136 @@ +describe('5-module-3-task', () => { + let carouselWrapper; + let carouselInner; + let carouselArrowRight; + let carouselArrowLeft; + + let clickEvent; + + beforeEach(() => { + carouselWrapper = document.createElement('div'); + carouselWrapper.setAttribute('data-carousel-holder', ''); + carouselWrapper.classList.add('container'); + carouselWrapper.innerHTML = ` + + `; + + document.body.append(carouselWrapper); + + let slideWidth = '500px'; + carouselInner = carouselWrapper.querySelector('.carousel__inner'); + carouselInner.style.width = slideWidth; + + let slides = carouselWrapper.querySelectorAll('.carousel__slide'); + + slides.forEach((slide) => { + slide.style.width = slideWidth; + let img = slide.querySelector('.carousel__img'); + + if (img) { + img.style.width = slideWidth; + } + }); + + carouselArrowRight = carouselWrapper.querySelector('.carousel__arrow_right'); + carouselArrowLeft = carouselWrapper.querySelector('.carousel__arrow_left'); + + clickEvent = new MouseEvent('click', { bubbles: true }); + + initCarousel(); + }); + + afterEach(() => { + carouselWrapper.remove(carouselWrapper); + }); + + + describe('переключение вперёд', () => { + it('при клике по кнопке "вперёд", должна переключать на один слайд вперёд', () => { + carouselArrowRight.dispatchEvent(clickEvent); + + expect(carouselInner.style.transform).toBe("translateX(-500px)"); + }); + }); + + describe('переключение назад', () => { + beforeEach(() => { + carouselArrowRight.dispatchEvent(clickEvent); + carouselArrowRight.dispatchEvent(clickEvent); + carouselArrowRight.dispatchEvent(clickEvent); + }); + + it('при клике по кнопке "назад", должна переключать на один слайд назад', () => { + carouselArrowLeft.dispatchEvent(clickEvent); + + expect(carouselInner.style.transform).toBe('translateX(-1000px)'); + }); + }); + + describe('скрытие стрелок переключения', () => { + it('должна по умолчанию скрыть стрелку переключения назад', () => { + expect(carouselArrowLeft.style.display).toBe('none'); + }); + + it('при достижении четвёртого слайда, должна скрыть стрелку переключения вперёд', () => { + carouselArrowRight.dispatchEvent(clickEvent); + carouselArrowRight.dispatchEvent(clickEvent); + carouselArrowRight.dispatchEvent(clickEvent); + + expect(carouselArrowRight.style.display).toBe('none'); + }); + }); + +}); diff --git a/6-module/1-task/README.md b/6-module/1-task/README.md new file mode 100644 index 0000000..7816e9d --- /dev/null +++ b/6-module/1-task/README.md @@ -0,0 +1,68 @@ +# Таблица с удаляемыми строками + +В этом задании нужно создать таблицу с возможностью удаления строк. + +Вы получаете данные в виде массива. + +Пример данных: +```js +let rows = [ + { + name: 'Вася', + age: 25, + salary: 1000, + city: 'Самара' + }, + { + name: 'Петя', + age: 30, + salary: 1500, + city: 'Москва' + } +]; +``` + +Напишите класс `UserTable`, который создаёт таблицу с этими данными: + +- Для каждого элемента массива должна быть отдельная строка в таблице. +- В конце каждой строки должна быть кнопка `[X]`, при клике на которую эта строка удаляется. +- Ссылку на корневой элемент `table` следует добавить в свойство `elem`. **Обращаем вашe внимание**: свойство `elem` не должно быть геттером(`get elem()`), который при каждом вызове создает новый DOM-елемент. Допускается геттер, который просто возвращает ссылку. + +Пример использования: + +```js +let table = new UserTable(rows); +document.body.appendChild(table.elem); +``` + +Структура, которая должна быть в HTML: + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + +
ИмяВозрастЗарплатаГород
Вася251000Самара
Петя251000Самара
+``` diff --git a/6-module/1-task/index.html b/6-module/1-task/index.html new file mode 100644 index 0000000..e8c4353 --- /dev/null +++ b/6-module/1-task/index.html @@ -0,0 +1,72 @@ + + + + + Очищаемая таблица + + + + +

Пример минимально необходимой структуры таблицы (только HTML) для прохождения тестов:

+
+ + + + + + + + + + + + + + + + + + + +
ИмяВозрастЗарплатаГород
Ilia251000Petrozavodsk
+
+ +

Результат выполнения вашего кода:

+ + + + + diff --git a/6-module/1-task/index.js b/6-module/1-task/index.js new file mode 100644 index 0000000..3ff7803 --- /dev/null +++ b/6-module/1-task/index.js @@ -0,0 +1,18 @@ +/** + * Компонент, который реализует таблицу + * с возможностью удаления строк + * + * Пример одного элемента, описывающего строку таблицы + * + * { + * name: 'Ilia', + * age: 25, + * salary: '1000', + * city: 'Petrozavodsk' + * } + * + */ +export default class UserTable { + constructor(rows) { + } +} diff --git a/6-module/1-task/style.css b/6-module/1-task/style.css new file mode 100644 index 0000000..dbd2a82 --- /dev/null +++ b/6-module/1-task/style.css @@ -0,0 +1,8 @@ +td { + border: 1px solid black; + padding: 6px; +} + +table { + border-collapse: collapse; +} \ No newline at end of file diff --git a/6-module/1-task/task.test.js b/6-module/1-task/task.test.js new file mode 100644 index 0000000..b027ffd --- /dev/null +++ b/6-module/1-task/task.test.js @@ -0,0 +1,56 @@ +import UserTable from './index.js'; + +describe('6-module-1-task', () => { + let userTable; + + let clickEvent; + + beforeEach(() => { + clickEvent = new MouseEvent('click', { bubbles: true }); + + let rows = [ + { + name: 'Вася', + age: 25, + salary: 1000, + city: 'Самара' + }, + { + name: 'Петя', + age: 30, + salary: 1500, + city: 'Москва' + } + ]; + + userTable = new UserTable(rows); + + document.body.append(userTable.elem); + }); + + afterEach(() => { + userTable.elem.remove(); + }); + + it('свойство elem возвращает один и тот же елемент, при каждом обращении', () => { + const elementFirstCall = userTable.elem; + const elementSecondCall = userTable.elem; + + expect(elementFirstCall).toBe(elementSecondCall); + }); + + it('компонент должен отрисовать всех пользователей', () => { + let rowsInHTMLlength = userTable.elem.querySelectorAll('tbody tr').length; + + expect(rowsInHTMLlength).toBe(2); + }); + + it('при клике на кнопку удаляется строка', () => { + let buttons = userTable.elem.querySelectorAll('button'); + + buttons[0].dispatchEvent(clickEvent); + buttons[1].dispatchEvent(clickEvent); + + expect(userTable.elem.querySelector('tbody tr')).toBeNull(); + }); +}); diff --git a/6-module/2-task/README.md b/6-module/2-task/README.md new file mode 100644 index 0000000..8f19220 --- /dev/null +++ b/6-module/2-task/README.md @@ -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 +
+
+ product + +
+
+
+ +
+
+``` + +На вёрстку можно посмотреть в файле `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(` + + + + + + + + + + + + + + +
Имя
Вася
Петя
+`) +``` +- Цена представлена в объекте товара, как число (например, вот так: `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`, а это потребуется в дальнейшем. diff --git a/6-module/2-task/index.css b/6-module/2-task/index.css new file mode 100644 index 0000000..2157614 --- /dev/null +++ b/6-module/2-task/index.css @@ -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; + } +} diff --git a/6-module/2-task/index.html b/6-module/2-task/index.html new file mode 100644 index 0000000..468452b --- /dev/null +++ b/6-module/2-task/index.html @@ -0,0 +1,30 @@ + + + + + + + Бангкок Экспресс: Карточка товара + + + + + +
+ + + + + diff --git a/6-module/2-task/index.js b/6-module/2-task/index.js new file mode 100644 index 0000000..c997ec9 --- /dev/null +++ b/6-module/2-task/index.js @@ -0,0 +1,4 @@ +export default class ProductCard { + constructor(product) { + } +} \ No newline at end of file diff --git a/6-module/2-task/static.html b/6-module/2-task/static.html new file mode 100644 index 0000000..0c10d1d --- /dev/null +++ b/6-module/2-task/static.html @@ -0,0 +1,29 @@ + + + + + + + Бангкок Экспресс: Карточка товара + + + + + +
+
+
+ product + €10.00 +
+
+
Laab kai chicken salad
+ +
+
+
+ + + diff --git a/6-module/2-task/task.test.js b/6-module/2-task/task.test.js new file mode 100644 index 0000000..64b1eb6 --- /dev/null +++ b/6-module/2-task/task.test.js @@ -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); + }); + }); +}); diff --git a/6-module/3-task/README.md b/6-module/3-task/README.md new file mode 100644 index 0000000..cb41cbd --- /dev/null +++ b/6-module/3-task/README.md @@ -0,0 +1,112 @@ +# Учебный проект: Карусель + +Создайте класс `Carousel`, описывающий компонент "Карусель". С данным компонентом интерфейса мы уже познакомились в предыдущем занятии, и для решения этой задачи, вам понадобятся ваши наработки. Главным отличием этой задачи будет использование компонентного подхода. + +В качестве аргумента в конструктор класса передаётся массив слайдов для отображения: + +```js +let slides = [ + { + name: 'Penang shrimp', // Название товара со слайда + price: 16, // Цена товара со слайда + image: 'penang_shrimp.png', // Название файла картинки со слайда + id: 'penang-shrimp' // Уникальный идентификатор товара со слайда + }, + { + name: 'Chicken cashew', + price: 14, + image: 'chicken_cashew.png', + id: 'chicken-cashew' + }, +]; + +let carousel = new Carousel(slides); +``` + +После этого в `carousel.elem` должен быть доступен корневой DOM-элемент карусели. + +На вёрстку можно посмотреть в файле `static.html`, а пример использования - в файле `index.html`. Вёрстка идентична вёрстке из предыдущей задачи про карусель, однако в этот раз вам нужно будет отрисовать её самим. + +## Отрисовка вёрстки компонента + +Как видно из вёрстки готовой карусели в файле `static.html`: +- корневой элемент компонента имеет класс `carousel` +- все слайды находятся внутри элемента с классом `carousel__inner` +- вёрстка одного слайда выглядит вот так: + +```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(` + + + + + + + + + + + + + + +
Имя
Вася
Петя
+`) +``` +- Цена представлена в объекте слайда, как число (например, вот так: `10`), но отобразить её нужно, во-первых, со значком валюты в начале - `€`, а во-вторых, с двумя символами после точки - `€10.00`. Чтобы получить два символа, воспользуйтесь методом числа `toFixed`, про который можно прочитать [вот в этой статье](https://learn.javascript.ru/number#okruglenie). +- Нужно дополнить путь к картинке слайда. Все картинки слайдов лежат в папке `/assets/images/carousel`. В тоже время, для каждого слайда, нам нужно прописать путь к конкретной картинке. Название картинки вы найдёте в свойстве `image` объекта слайда. В итоге у вас должен получится путь вида `/assets/images/carousel/penang_shrimp.png`, где `penang_shrimp.png` вы возьмёте из свойства `image`. Этот путь нужно прописать в атрибут `src` картинки `img` с CSS классом `carousel__img`. +- Созданный DOM-елемент вашей карусели, необходимо сохранить в свойство `elem` вашего класса `Carousel`, для того чтобы его можно было использовать вот так (см. пример в `index.html`): + +```js +let carousel = new Carousel(slides); +console.log(carousel.elem); // Корневой HTML элемента карусели +``` + +- Cвойство `elem` не должно быть геттером(`get elem()`), который при каждом вызове создает новый DOM-елемент, так как ваш компонент могут использовать несколько раз. Поэтому допускается геттер, который просто возвращает созданный DOM-елемент, к примеру: +```js +get elem() { + return this._container; +} +``` + +## Переключение слайдов по стрелкам + +Требования к переключениям слайдов точно такие же, как в предыдущей задаче про карусель. Отличие в том, что здесь количество слайдов не фиксированное, а может быть любым. Это нужно будет учесть в решении, а в остальном вы можете переиспользовать ваш код. + +## Событие при клике на "+" + +Кроме показа карусели и переключения слайдов, нужно генерировать событие при клике по кнопке добавления `"+"`. + +В нашем проекте товары можно будет добавлять не только из "Карточки товара", но и из "Карусели". + +А именно, при клике пользователя по кнопке с классом `carousel__button` генерировать пользовательское событие на корневом HTML элементе компонента (который хранится в свойстве `elem`), такого вида: + +```js +new CustomEvent("product-add", { // имя события должно быть именно "product-add" + detail: slide.id, // Уникальный идентификатора товара из объекта слайда + bubbles: true // это событие всплывает - это понадобится в дальнейшем +} +``` + +Как вы видите, для генерации такого события необходим уникальный идентификатор товара (`slide.id`). Для простоты его можно хранить в дата-атрибуте. К примеру, мы используем атрибут `data-id` на элементе слайда. + +Про пользовательские события можно прочитать в статье - [Генерация пользовательских событий](https://learn.javascript.ru/dispatch-events). + +**(!!!)** Обращаем ваше внимание, что это событие должно **ОБЯЗАТЕЛЬНО** всплывать. Для этого не забудьте передать свойство `bubbles: true` в опциях в момент создания объекта события, как это показано выше. Если этого не сделать, событие невозможно будет отловить на элементе `body`, а это потребуется в дальнейшем. diff --git a/6-module/3-task/index.css b/6-module/3-task/index.css new file mode 100644 index 0000000..afbc2bf --- /dev/null +++ b/6-module/3-task/index.css @@ -0,0 +1,152 @@ +.carousel { + height: var(--carousel-height); + position: relative; + overflow: hidden; +} + +.carousel__caption { + position: absolute; + z-index: 2; + right: 0; + bottom: 0; + height: 70px; + background-color: var(--color-black-dark); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.carousel__price { + position: absolute; + right: 0; + bottom: 100%; + 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; +} + +.carousel__title { + text-align: center; + font-weight: 500; + font-size: 21px; + font-style: italic; + line-height: 1.2; + width: 100%; + padding: 0 20px; +} + +.carousel__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; + transition: 0.2s all; +} + +.carousel__button:hover, +.carousel__button:active { + background-color: var(--color-yellow-dark); +} + +.carousel__inner { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + position: absolute; + left: 0; + top: 0; + transition: all 1s ease; +} + +.carousel__slide { + width: 100%; + min-width: 100%; + height: 100%; + position: relative; + margin: 0; +} + +.carousel__arrow { + position: absolute; + z-index: 3; + bottom: 0; + top: 50%; + transform: translate(0, -50%); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 70px; + height: 70px; +} + +.carousel__arrow_right { + right: 0; +} + +.carousel__arrow_left { + left: 0; +} + +.carousel__arrow img, +.carousel__arrow svg { + max-width: 20px; +} + +.carousel__img { + min-width: 100%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +@media all and (max-width: 767px) { + .carousel { + padding-bottom: 57px; + } + + .carousel__caption { + left: 0; + height: 57px; + } + + .carousel__arrow { + bottom: 57px; + } + + .carousel__arrow_right img, + .carousel__arrow_right svg { + margin-top: 0; + } + + .carousel__arrow_left img, + .carousel__arrow_left svg { + margin-bottom: 0; + } +} + +@media (max-width: 992px) { + .carousel__img { + position: absolute; + top: 0; + left: 0; + transform: none; + } +} diff --git a/6-module/3-task/index.html b/6-module/3-task/index.html new file mode 100644 index 0000000..808fc47 --- /dev/null +++ b/6-module/3-task/index.html @@ -0,0 +1,28 @@ + + + + + + + Бангкок Экспресс: Карусель + + + + + + +
+
+ + + + + diff --git a/6-module/3-task/index.js b/6-module/3-task/index.js new file mode 100644 index 0000000..2f098a1 --- /dev/null +++ b/6-module/3-task/index.js @@ -0,0 +1,7 @@ +import createElement from '../../assets/lib/create-element.js'; + +export default class Carousel { + constructor(slides) { + this.slides = slides; + } +} diff --git a/6-module/3-task/slides.js b/6-module/3-task/slides.js new file mode 100644 index 0000000..2aa779a --- /dev/null +++ b/6-module/3-task/slides.js @@ -0,0 +1,26 @@ +export default [ + { + name: 'Penang shrimp', + price: 16, + image: 'penang_shrimp.png', + id: 'penang-shrimp' + }, + { + name: 'Chicken cashew', + price: 14, + image: 'chicken_cashew.png', + id: 'chicken-cashew' + }, + { + name: 'Red curry veggies', + price: 12.5, + image: 'red_curry_vega.png', + id: 'red-curry-veggies' + }, + { + name: 'Chicken springrolls', + price: 6.5, + image: 'chicken_loempias.png', + id: 'chicken-springrolls' + } +]; diff --git a/6-module/3-task/static.html b/6-module/3-task/static.html new file mode 100644 index 0000000..1f587d7 --- /dev/null +++ b/6-module/3-task/static.html @@ -0,0 +1,71 @@ + + + + + +
+ + + + +
+ + diff --git a/6-module/3-task/task.test.js b/6-module/3-task/task.test.js new file mode 100644 index 0000000..2cbb5bc --- /dev/null +++ b/6-module/3-task/task.test.js @@ -0,0 +1,133 @@ +import slides from './slides.js'; +import Carousel from './index.js'; + +describe('6-module-3-task', () => { + let sut; + + let carouselInner; + let carouselArrowRight; + let carouselArrowLeft; + + let clickEvent; + let testSlides; + + beforeEach(() => { + testSlides = slides.slice(1); + + sut = new Carousel(testSlides); + document.body.append(sut.elem); + + let slideWidth = '500px'; + + carouselInner = sut.elem.querySelector('.carousel__inner'); + sut.elem.style.width = slideWidth; + + let slidesElements = sut.elem.querySelectorAll('.carousel__slide'); + + slidesElements.forEach((slideElement) => { + slideElement.style.width = slideWidth; + let img = slideElement.querySelector('.carousel__img'); + + if (img) { + img.style.width = slideWidth; + } + }); + + carouselArrowRight = sut.elem.querySelector('.carousel__arrow_right'); + carouselArrowLeft = sut.elem.querySelector('.carousel__arrow_left'); + + clickEvent = new MouseEvent('click', { bubbles: true }); + }); + + afterEach(() => { + sut.elem.remove(); + }); + + describe('отрисовка вёрстки после создания', () => { + it('должна добавлять корневой элемент в свойство "elem"', () => { + expect(sut.elem.classList.contains('carousel')).toBe(true); + }); + + it('должна отрисовать все слайды', () => { + let slidesElements = sut.elem.querySelectorAll('.carousel__slide'); + + expect(slidesElements.length).toBe(3); + }); + }); + + describe('переключение слайдов', () => { + describe('переключение вперёд', () => { + it('при клике по кнопке "вперёд", должна переключать на один слайд вперёд', () => { + carouselArrowRight.dispatchEvent(clickEvent); + + expect(carouselInner.style.transform).toBe("translateX(-500px)"); + }); + }); + + describe('переключение назад', () => { + beforeEach(() => { + carouselArrowRight.dispatchEvent(clickEvent); + carouselArrowRight.dispatchEvent(clickEvent); + }); + + it('при клике по кнопке "назад", должна переключать на один слайд назад', () => { + carouselArrowLeft.dispatchEvent(clickEvent); + + expect(carouselInner.style.transform).toBe('translateX(-500px)'); + }); + }); + + describe('скрытие стрелок переключения', () => { + it('в исходном состоянии скрывает стрелку переключения назад', () => { + expect(carouselArrowLeft.style.display).toBe('none'); + }); + + it('при достижении крайнего слайда, должна скрыть стрелку переключения вперёд', () => { + carouselArrowRight.dispatchEvent(clickEvent); + carouselArrowRight.dispatchEvent(clickEvent); + + expect(carouselArrowRight.style.display).toBe('none'); + }); + }); + }); + + describe('генерация события добавления в корзину("product-add")', () => { + let productAddEvent; + + beforeEach(() => { + productAddEvent = null; + + document.body.addEventListener('product-add', (event) => { + productAddEvent = event; + }, { once: true }); + }); + + afterEach(() => { + productAddEvent = null; + }); + + it('после клика по кнопке, должно быть создано событие', () => { + let addButton = sut.elem.querySelector('.carousel__button'); + addButton.dispatchEvent(clickEvent); + + expect(productAddEvent instanceof CustomEvent).toBe(true); + }); + + it('созданное событие должно содержать в себе уникальный идентификатор товара("id") c 1-ого слайда,' + + ' если кликнули на 1 слайд', () => { + let addButton = sut.elem.querySelector('.carousel__button'); + addButton.dispatchEvent(clickEvent); + + expect(productAddEvent.detail).toBe(testSlides[0].id); + }); + + it('созданное событие должно содержать в себе уникальный идентификатор товара("id") c 2-ого слайда,' + + ' если кликнули на 2 слайд', () => { + carouselArrowRight.dispatchEvent(clickEvent); + let addButtons = sut.elem.querySelectorAll('.carousel__button'); + addButtons[1].dispatchEvent(clickEvent); + + expect(productAddEvent.detail).toBe(testSlides[1].id); + }); + }); +});