This commit is contained in:
Egor Deev 2025-11-19 11:27:52 +03:00 committed by GitHub
parent 02bab4c8e3
commit 66738488d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1554 additions and 0 deletions

353
labs/lab-05/dishes.js Normal file
View file

@ -0,0 +1,353 @@
const dishes = [
{
keyword: "mushroom-soup",
name: "Грибной крем-суп",
price: 280,
category: "soup",
count: "300 мл",
image: "img/soup/mushroom-soup.jpg",
kind: "veg"
},
{
keyword: "beet-soup",
name: "Традиционный борщ",
price: 240,
category: "soup",
count: "350 мл",
image: "img/soup/beet-soup.jpg",
kind: "meat"
},
{
keyword: "chicken-noodle-soup",
name: "Куриный бульон с лапшой",
price: 220,
category: "soup",
count: "350 мл",
image: "img/soup/chicken-noodle-soup.jpg",
kind: "meat"
},
{
keyword: "minestrone-soup",
name: "Итальянский суп минестроне",
price: 260,
category: "soup",
count: "350 мл",
image: "img/soup/minestrone-soup.jpg",
kind: "veg"
},
{
keyword: "tomato-soup",
name: "Томатный крем-суп",
price: 250,
category: "soup",
count: "300 мл",
image: "img/soup/tomato-soup.jpg",
kind: "veg"
},
{
keyword: "pumpkin-soup",
name: "Тыквенный суп с имбирем",
price: 270,
category: "soup",
count: "300 мл",
image: "img/soup/pumpkin-soup.jpg",
kind: "veg"
},
{
keyword: "fish-soup",
name: "Уха из лосося",
price: 320,
category: "soup",
count: "350 мл",
image: "img/soup/fish-soup.jpg",
kind: "fish"
},
{
keyword: "seafood-soup",
name: "Суп том-ям с морепродуктами",
price: 340,
category: "soup",
count: "350 мл",
image: "img/soup/seafood-soup.jpg",
kind: "fish"
},
{
keyword: "french-onion-soup",
name: "Французский луковый суп",
price: 300,
category: "soup",
count: "350 мл",
image: "img/soup/french-onion-soup.jpg",
kind: "veg"
},
{
keyword: "vegan-bowl-chickpeas",
name: "Веганский боул с нутом",
price: 390,
category: "main-course",
count: "350 г",
image: "img/main-dishes/vegan-bowl-chickpeas.jpg",
kind: "veg"
},
{
keyword: "grilled-chicken-vegetables",
name: "Куриная грудка с овощами гриль",
price: 450,
category: "main-course",
count: "350 г",
image: "img/main-dishes/grilled-chicken-vegetables.jpg",
kind: "meat"
},
{
keyword: "salmon-quinoa-avocado",
name: "Лосось с киноа и авокадо",
price: 620,
category: "main-course",
count: "380 г",
image: "img/main-dishes/salmon-quinoa-avocado.jpg",
kind: "fish"
},
{
keyword: "seafood-pasta",
name: "Паста с морепродуктами",
price: 550,
category: "main-course",
count: "330 г",
image: "img/main-dishes/seafood-pasta.jpg",
kind: "fish"
},
{
keyword: "teriyaki-chicken-noodles",
name: "Рисовая лапша с курицей терияки",
price: 420,
category: "main-course",
count: "340 г",
image: "img/main-dishes/teriyaki-chicken-noodles.jpg",
kind: "meat"
},
{
keyword: "beef-salad",
name: "Теплый салат с говядиной",
price: 490,
category: "main-course",
count: "320 г",
image: "img/main-dishes/beef-salad.jpg",
kind: "meat"
},
{
keyword: "vegetable-stir-fry",
name: "Овощное рагу с тофу",
price: 380,
category: "main-course",
count: "350 г",
image: "img/main-dishes/vegetable-stir-fry.jpg",
kind: "veg"
},
{
keyword: "duck-orange-sauce",
name: "Утка в апельсиновом соусе",
price: 580,
category: "main-course",
count: "360 г",
image: "img/main-dishes/duck-orange-sauce.jpg",
kind: "meat"
},
{
keyword: "mushroom-risotto",
name: "Ризотто с белыми грибами",
price: 410,
category: "main-course",
count: "340 г",
image: "img/main-dishes/mushroom-risotto.jpg",
kind: "veg"
},
{
keyword: "berry-juice",
name: "Ягодный морс",
price: 120,
category: "drink",
count: "300 мл",
image: "img/drinks/berry-juice.jpg",
kind: "cold"
},
{
keyword: "water",
name: "Минеральная вода",
price: 80,
category: "drink",
count: "500 мл",
image: "img/drinks/water.jpg",
kind: "cold"
},
{
keyword: "spinach-smoothie",
name: "Зеленый смузи с шпинатом",
price: 180,
category: "drink",
count: "350 мл",
image: "img/drinks/spinach-smoothie.jpg",
kind: "cold"
},
{
keyword: "ginger-lemonade",
name: "Имбирный лимонад",
price: 140,
category: "drink",
count: "300 мл",
image: "img/drinks/ginger-lemonade.jpg",
kind: "cold"
},
{
keyword: "mango-smoothie",
name: "Смузи манго-маракуйя",
price: 170,
category: "drink",
count: "350 мл",
image: "img/drinks/mango-smoothie.jpg",
kind: "cold"
},
{
keyword: "orange-juice",
name: "Свежевыжатый апельсиновый сок",
price: 150,
category: "drink",
count: "300 мл",
image: "img/drinks/orange-juice.jpg",
kind: "cold"
},
{
keyword: "cappuccino",
name: "Капучино",
price: 150,
category: "drink",
count: "300 мл",
image: "img/drinks/cappuccino.jpg",
kind: "hot"
},
{
keyword: "green-tea",
name: "Зеленый чай",
price: 100,
category: "drink",
count: "400 мл",
image: "img/drinks/green-tea.jpg",
kind: "hot"
},
{
keyword: "hot-chocolate",
name: "Горячий шоколад",
price: 160,
category: "drink",
count: "300 мл",
image: "img/drinks/hot-chocolate.jpg",
kind: "hot"
},
{
keyword: "shrimp-salad",
name: "Салат с креветками и авокадо",
price: 380,
category: "salad",
count: "250 г",
image: "img/salads/shrimp-salad.jpg",
kind: "fish"
},
{
keyword: "caesar-salad",
name: "Цезарь с курицей",
price: 320,
category: "salad",
count: "280 г",
image: "img/salads/caesar-salad.jpg",
kind: "meat"
},
{
keyword: "greek-salad",
name: "Греческий салат",
price: 280,
category: "salad",
count: "260 г",
image: "img/salads/greek-salad.jpg",
kind: "veg"
},
{
keyword: "caprese-salad",
name: "Капрезе с моцареллой",
price: 290,
category: "salad",
count: "240 г",
image: "img/salads/caprese-salad.jpg",
kind: "veg"
},
{
keyword: "quinoa-salad",
name: "Салат с киноа и овощами",
price: 310,
category: "salad",
count: "270 г",
image: "img/salads/quinoa-salad.jpg",
kind: "veg"
},
{
keyword: "beetroot-salad",
name: "Салат со свеклой и орехами",
price: 260,
category: "salad",
count: "250 г",
image: "img/salads/beetroot-salad.jpg",
kind: "veg"
},
{
keyword: "chocolate-mousse",
name: "Шоколадный мусс",
price: 180,
category: "dessert",
count: "100 г",
image: "img/desserts/chocolate-mousse.jpg",
kind: "small"
},
{
keyword: "panna-cotta",
name: "Панна-котта с ягодным соусом",
price: 200,
category: "dessert",
count: "120 г",
image: "img/desserts/panna-cotta.jpg",
kind: "small"
},
{
keyword: "lemon-tart",
name: "Лимонный тарт",
price: 190,
category: "dessert",
count: "110 г",
image: "img/desserts/lemon-tart.jpg",
kind: "small"
},
{
keyword: "cheesecake",
name: "Чизкейк Нью-Йорк",
price: 280,
category: "dessert",
count: "150 г",
image: "img/desserts/cheesecake.jpg",
kind: "medium"
},
{
keyword: "tiramisu",
name: "Тирамису классический",
price: 290,
category: "dessert",
count: "160 г",
image: "img/desserts/tiramisu.jpg",
kind: "medium"
},
{
keyword: "chocolate-cake",
name: "Шоколадный торт",
price: 350,
category: "dessert",
count: "200 г",
image: "img/desserts/chocolate-cake.jpg",
kind: "large"
}
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
labs/lab-05/img/main.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

101
labs/lab-05/index.html Normal file
View file

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://deev.space/media/favicon.ico" type="image/x-icon">
<title>ЭкоЛанч - Доставка здоровых бизнес-ланчей в Москве</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="index.html" id="active">Главная</a>
<a href="menu.html">Собрать ланч</a>
<a href="delivery.html">Доставка</a>
<a href="about.html">О нас</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main>
<section class="about-company">
<h2>О компании ЭкоЛанч</h2>
<img src="img/main.jpg" alt="Здоровая еда в контейнерах">
<p>ЭкоЛанч - это современная компания по доставке здоровых бизнес-ланчей в офисы Москвы. Наша миссия - сделать правильное питание доступным для занятых людей, которые ценят свое время и здоровье.</p>
<p>Мы используем только свежие продукты от проверенных поставщиков, а наши повара готовят блюда каждое утро. Все ланчи упакованы в экологичную биоразлагаемую упаковку, потому что мы заботимся не только о вашем здоровье, но и о здоровье нашей планеты.</p>
</section>
<section class="advantages">
<h2>Наши преимущества</h2>
<ul>
<li>Быстрая доставка в течение 60 минут по всей Москве в пределах МКАД</li>
<li>Свежие блюда, приготовленные утром из качественных продуктов</li>
<li>Сбалансированное меню, разработанное профессиональными диетологами</li>
<li>Экологичная упаковка, безопасная для окружающей среды</li>
<li>Гибкая система скидок для корпоративных клиентов и постоянных заказчиков</li>
</ul>
</section>
<section class="popular-dishes">
<h2>Самые популярные блюда</h2>
<table>
<tr>
<th>Название блюда</th>
<th>Описание</th>
<th>Калорийность</th>
<th>Цена</th>
</tr>
<tr>
<td>Куриная грудка с овощами гриль</td>
<td>Нежная куриная грудка с цукини, баклажанами и болгарским перцем</td>
<td>420 ккал</td>
<td>450 руб.</td>
</tr>
<tr>
<td>Лосось с киноа и авокадо</td>
<td>Запеченный лосось с гарниром из киноа и свежим авокадо</td>
<td>520 ккал</td>
<td>620 руб.</td>
</tr>
<tr>
<td>Теплый салат с говядиной</td>
<td>Сочная говядина с микс-салатом, томатами черри и кедровыми орешками</td>
<td>380 ккал</td>
<td>490 руб.</td>
</tr>
<tr>
<td>Веганский боул с нутом</td>
<td>Запеченный нут, хумус, овощи и тахини соус</td>
<td>450 ккал</td>
<td>390 руб.</td>
</tr>
<tr>
<td>Паста с морепродуктами</td>
<td>Паста из твердых сортов пшеницы с креветками, мидиями и томатным соусом</td>
<td>480 ккал</td>
<td>550 руб.</td>
</tr>
<tr>
<td>Рисовая лапша с курицей терияки</td>
<td>Рисовая лапша wok с куриным филе в соусе терияки и овощами</td>
<td>460 ккал</td>
<td>420 руб.</td>
</tr>
</table>
</section>
</main>
<footer id="contacts">
<p><b>Контактная информация</b></p>
<p>Телефон: <a href="tel:+79993737737">+7 (999) 373-77-37</a></p>
<p>Email: <a href="mailto:egor@deev.space">egor@deev.space</a></p>
<p>Адрес: г. Москва, ул. Михалковская, д. 7, к. 1, офис 813А</p>
<p>Режим работы: Пн-Пт с 8:00 до 20:00</p>
</footer>
</body>
</html>

158
labs/lab-05/menu.html Normal file
View file

@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://deev.space/media/favicon.ico" type="image/x-icon">
<title>Собрать ланч - ЭкоЛанч</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="index.html">Главная</a>
<a href="menu.html" id="active">Собрать ланч</a>
<a href="delivery.html">Доставка</a>
<a href="about.html">О нас</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main>
<section>
<div class="section-header">
<h2>Супы</h2>
<div class="filter-buttons">
<button class="filter-btn" data-kind="fish">Рыбный</button>
<button class="filter-btn" data-kind="meat">Мясной</button>
<button class="filter-btn" data-kind="veg">Вегетарианский</button>
</div>
</div>
<div class="dishes-grid" id="soup-section"></div>
</section>
<section>
<div class="section-header">
<h2>Главные блюда</h2>
<div class="filter-buttons">
<button class="filter-btn" data-kind="fish">Рыбное</button>
<button class="filter-btn" data-kind="meat">Мясное</button>
<button class="filter-btn" data-kind="veg">Вегетарианское</button>
</div>
</div>
<div class="dishes-grid" id="main-course-section"></div>
</section>
<section>
<div class="section-header">
<h2>Салаты и стартеры</h2>
<div class="filter-buttons">
<button class="filter-btn" data-kind="fish">Рыбный</button>
<button class="filter-btn" data-kind="meat">Мясной</button>
<button class="filter-btn" data-kind="veg">Вегетарианский</button>
</div>
</div>
<div class="dishes-grid" id="salad-section"></div>
</section>
<section>
<div class="section-header">
<h2>Напитки</h2>
<div class="filter-buttons">
<button class="filter-btn" data-kind="cold">Холодный</button>
<button class="filter-btn" data-kind="hot">Горячий</button>
</div>
</div>
<div class="dishes-grid" id="drink-section"></div>
</section>
<section>
<div class="section-header">
<h2>Десерты</h2>
<div class="filter-buttons">
<button class="filter-btn" data-kind="small">Маленькая порция</button>
<button class="filter-btn" data-kind="medium">Средняя порция</button>
<button class="filter-btn" data-kind="large">Большая порция</button>
</div>
</div>
<div class="dishes-grid" id="dessert-section"></div>
</section>
</main>
<section class="order-form">
<h2>Оформить заказ</h2>
<form action="https://httpbin.org/post" method="POST">
<div class="form-container">
<div class="order-section">
<h3>Ваш заказ</h3>
<div id="order-summary">
<p class="empty-order">Ничего не выбрано</p>
</div>
</div>
<div class="customer-section">
<h3>Данные для доставки</h3>
<label for="name">Имя</label>
<input type="text" name="name" id="name" placeholder="Введите ваше имя" required>
<label for="email">Email</label>
<input type="email" name="email" id="email" placeholder="Введите ваш email" required>
<div class="checkbox-group">
<input type="checkbox" name="subscribe" id="subscribe" checked>
<label for="subscribe">Подписаться на рассылку</label>
</div>
<label for="phone">Телефон</label>
<input type="tel" name="phone" id="phone" placeholder="Введите ваш телефон" required>
<label for="address">Адрес доставки</label>
<input type="text" name="address" id="address" placeholder="Введите адрес доставки" required>
<small class="form-hint">Доставка осуществляется только по Москве</small>
<div class="radio-group">
<label>Время доставки</label>
<div>
<input type="radio" name="delivery-time-type" id="asap" value="asap" required>
<label for="asap">Как можно скорее</label>
</div>
<div>
<input type="radio" name="delivery-time-type" id="specific-time" value="specific-time" required>
<label for="specific-time">К определенному времени</label>
</div>
</div>
<label for="delivery-time">Указать время</label>
<input type="time" name="delivery-time" id="delivery-time" min="07:00" max="23:00" step="300">
<small class="form-hint">Доступное время доставки с 7:00 до 23:00</small>
<label for="comment">Комментарий к заказу</label>
<textarea name="comment" id="comment" placeholder="Введите комментарий к заказу"></textarea>
<div class="form-buttons">
<button type="reset">Сбросить</button>
<button type="submit">Отправить заказ</button>
</div>
</div>
</div>
</form>
</section>
<footer id="contacts">
<p><b>Контактная информация</b></p>
<p>Телефон: <a href="tel:+79993737737">+7 (999) 373-77-37</a></p>
<p>Email: <a href="mailto:egor@deev.space">egor@deev.space</a></p>
<p>Адрес: г. Москва, ул. Михалковская, д. 7, к. 1, офис 813А</p>
<p>Режим работы: Пн-Пт с 8:00 до 20:00</p>
</footer>
<script src="dishes.js"></script>
<script src="menu.js"></script>
</body>
</html>

259
labs/lab-05/menu.js Normal file
View file

@ -0,0 +1,259 @@
const selectedDishes = {
soup: null,
'main-course': null,
salad: null,
drink: null,
dessert: null
};
let activeFilters = {
soup: null,
'main-course': null,
salad: null,
drink: null,
dessert: null
};
function sortDishes() {
dishes.sort(function(a, b) {
return a.name.localeCompare(b.name, 'ru');
});
}
function displayDishes() {
const soupSection = document.getElementById('soup-section');
const mainCourseSection = document.getElementById('main-course-section');
const saladSection = document.getElementById('salad-section');
const drinkSection = document.getElementById('drink-section');
const dessertSection = document.getElementById('dessert-section');
soupSection.innerHTML = '';
mainCourseSection.innerHTML = '';
saladSection.innerHTML = '';
drinkSection.innerHTML = '';
dessertSection.innerHTML = '';
dishes.forEach(function(dish) {
const dishCard = createDishCard(dish);
if (dish.category === 'soup') {
if (!activeFilters.soup || dish.kind === activeFilters.soup) {
soupSection.insertAdjacentHTML('beforeend', dishCard);
}
} else if (dish.category === 'main-course') {
if (!activeFilters['main-course'] ||
dish.kind === activeFilters['main-course']) {
mainCourseSection.insertAdjacentHTML('beforeend', dishCard);
}
} else if (dish.category === 'salad') {
if (!activeFilters.salad || dish.kind === activeFilters.salad) {
saladSection.insertAdjacentHTML('beforeend', dishCard);
}
} else if (dish.category === 'drink') {
if (!activeFilters.drink || dish.kind === activeFilters.drink) {
drinkSection.insertAdjacentHTML('beforeend', dishCard);
}
} else if (dish.category === 'dessert') {
if (!activeFilters.dessert ||
dish.kind === activeFilters.dessert) {
dessertSection.insertAdjacentHTML('beforeend', dishCard);
}
}
});
addDishClickHandlers();
}
function createDishCard(dish) {
return `
<div class="dish-card" data-dish="${dish.keyword}">
<img src="${dish.image}" alt="${dish.name}">
<p class="dish-price">${dish.price} руб.</p>
<p class="dish-name">${dish.name}</p>
<p class="dish-weight">${dish.count}</p>
<button>Добавить</button>
</div>
`;
}
function addDishClickHandlers() {
const dishCards = document.querySelectorAll('.dish-card');
dishCards.forEach(function(card) {
card.addEventListener('click', function() {
const keyword = this.dataset.dish;
const dish = dishes.find(function(d) {
return d.keyword === keyword;
});
if (dish) {
selectDish(dish);
}
});
});
}
function selectDish(dish) {
selectedDishes[dish.category] = dish;
updateOrderSummary();
}
function updateOrderSummary() {
const orderSummary = document.getElementById('order-summary');
const hasSelection = selectedDishes.soup ||
selectedDishes['main-course'] ||
selectedDishes.salad ||
selectedDishes.drink ||
selectedDishes.dessert;
if (!hasSelection) {
orderSummary.innerHTML = '<p class="empty-order">Ничего не выбрано</p>';
return;
}
let summaryHTML = '';
let totalPrice = 0;
if (selectedDishes.soup) {
summaryHTML += `
<div class="order-category">
<h3>Суп</h3>
<div class="order-item">
<span>${selectedDishes.soup.name}</span>
<span>${selectedDishes.soup.price} руб.</span>
</div>
</div>
`;
totalPrice += selectedDishes.soup.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Суп</h3>
<p class="not-selected">Блюдо не выбрано</p>
</div>
`;
}
if (selectedDishes['main-course']) {
summaryHTML += `
<div class="order-category">
<h3>Главное блюдо</h3>
<div class="order-item">
<span>${selectedDishes['main-course'].name}</span>
<span>${selectedDishes['main-course'].price} руб.</span>
</div>
</div>
`;
totalPrice += selectedDishes['main-course'].price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Главное блюдо</h3>
<p class="not-selected">Блюдо не выбрано</p>
</div>
`;
}
if (selectedDishes.salad) {
summaryHTML += `
<div class="order-category">
<h3>Салат</h3>
<div class="order-item">
<span>${selectedDishes.salad.name}</span>
<span>${selectedDishes.salad.price} руб.</span>
</div>
</div>
`;
totalPrice += selectedDishes.salad.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Салат</h3>
<p class="not-selected">Блюдо не выбрано</p>
</div>
`;
}
if (selectedDishes.drink) {
summaryHTML += `
<div class="order-category">
<h3>Напиток</h3>
<div class="order-item">
<span>${selectedDishes.drink.name}</span>
<span>${selectedDishes.drink.price} руб.</span>
</div>
</div>
`;
totalPrice += selectedDishes.drink.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Напиток</h3>
<p class="not-selected">Напиток не выбран</p>
</div>
`;
}
if (selectedDishes.dessert) {
summaryHTML += `
<div class="order-category">
<h3>Десерт</h3>
<div class="order-item">
<span>${selectedDishes.dessert.name}</span>
<span>${selectedDishes.dessert.price} руб.</span>
</div>
</div>
`;
totalPrice += selectedDishes.dessert.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Десерт</h3>
<p class="not-selected">Десерт не выбран</p>
</div>
`;
}
summaryHTML += `
<div class="order-total">
<h3>Стоимость заказа</h3>
<p class="total-price">${totalPrice} руб.</p>
</div>
`;
orderSummary.innerHTML = summaryHTML;
}
function setupFilters() {
const filterButtons = document.querySelectorAll('.filter-btn');
filterButtons.forEach(function(button) {
button.addEventListener('click', function() {
const kind = this.dataset.kind;
const section = this.closest('section');
const category = section.querySelector('.dishes-grid').id
.replace('-section', '');
if (this.classList.contains('active')) {
this.classList.remove('active');
activeFilters[category] = null;
} else {
const sectionButtons = section
.querySelectorAll('.filter-btn');
sectionButtons.forEach(function(btn) {
btn.classList.remove('active');
});
this.classList.add('active');
activeFilters[category] = kind;
}
displayDishes();
});
});
}
document.addEventListener('DOMContentLoaded', function() {
sortDishes();
displayDishes();
setupFilters();
});

View file

@ -0,0 +1,32 @@
DOM и события
1. Что такое DOM?
2. Какие есть способы получения элементов из DOM?
3. Что такое событие?
4. Как добавить обработчик события?
5. Примеры свойств в объекте event.
6. Как при добавлении обработчика события с помощью addEventListener убедиться, что он сработает только один раз?
7. Примеры методов у элементов DOM.
8. Какие методы из DOM позволяют создавать и добавлять элементы
9. Назовите фазы жизненного цикла события.
10. На какой фазе изначально срабатывают обработчики событий?
11. Всплывают ли события blur и focus?
12. Как остановить всплытие события и когда это может понадобиться?
13. Как работает делегирование событий?
14. Что такое data-атрибуты и как их использовать?
15. В чём разница между innerHTML, outerHTML, textContent и innerText?
16. Что такое DocumentFragment и для чего он используется?

View file

@ -0,0 +1,105 @@
# Лабораторная работа №5. Добавление категорий и фильтров
---
Добавьте новые блюда и категории. Создайте фильтры для каждой категории.
## Порядок выполнения
Примерные макеты разделов:
* Раздел "Выберите суп"
* Раздел "Выберите главное блюдо"
* Раздел "Выберите салат или стартер"
* Раздел "Выберите напиток"
* Раздел "Выберите десерт"
* Раздел "Сделать заказ"
[Полный макет](https://lms.mospolytech.ru/pluginfile.php/1314142/mod_assign/intro/mockupfull.png "Полный макет") страницы:
1. Для каждой категории блюд добавьте блок с фильтрами (фильтры - это кнопки).
Фильтры для категорий:
|Категория|Фильтры|
|---|---|
|Суп|рыбный <br>мясной <br>вегетарианский|
|Главное блюдо|рыбное <br>мясное <br>вегетарианское|
|Напиток|холодный <br>горячий|
У каждой кнопки должен быть data-атрибут - data-kind. Значение data-kind - это название фильтра на английском. Например, для рыбных супов data-kind может быть со значением "fish".
2. Для каждого объекта с блюдом добавьте новое свойство kind. Оно будет хранить значение для фильтрации. Значение kind должно соответствовать значению data-kind одного из фильтров.
Например, если data-kind для вегетарианских супов - "veg", свойство kind супа "Гаспачо" тоже будет "veg".
3. Добавьте новые категории блюд на страницу.
Добавьте еще 2 категории блюд:
- Салаты и стартеры
- Десерты
Структура блоков с новыми категориями должна быть такой же как у остальных категорий. Сейчас новые блоки не содержат блюд, вы будете заполнять их в следующем шаге.
В данные категории также необходимо добавить блок с фильтрами. Фильтры:
|Категория|Фильтры|
|---|---|
|Салат или стартер|рыбный <br>мясной <br>вегетарианский|
|Десерты|маленькая порция <br>средняя порция <br>большая порция|
4. Добавьте новые блюда.
Теперь необходимо добавить новые блюда во все существующие категории. В каждой категории должно быть по 6 блюд.
Добавьте в массив все недостающие блюда. Не забудьте указать все свойства, включая kind.
В каждом разделе должны быть представлены блюда всех возможных категорий для данного раздела. Ниже представлена таблица с указанием количества блюд каждой категории для соответствующих им разделов меню:
| Блюдо | Категория | Количество |
| ----------------- | ---------------- | ---------- |
| Суп | рыбный | 2 |
| Суп | мясной | 2 |
| Суп | вегетарианский | 2 |
| Главное блюдо | рыбное | 2 |
| Главное блюдо | мясное | 2 |
| Главное блюдо | вегетарианское | 2 |
| Салат или стартер | рыбный | 1 |
| Салат или стартер | мясной | 1 |
| Салат или стартер | вегетарианский | 4 |
| Напиток | холодный | 3 |
| Напиток | горячий | 3 |
| Десерт | маленькая порция | 3 |
| Десерт | средняя порция | 2 |
| Десерт | большая порция | 1 |
То есть, если сейчас в разделе с главными блюдами у вас добавлено 2 рыбных и 1 мясное блюдо, вам нужно добавить еще 1 мясное и 2 вегетарианских.
Изображения с блюдами можно скачать по [ссылке](https://lms.mospolytech.ru/pluginfile.php/1314142/mod_assign/intro/menu.zip?time=1729893070876 "ссылке").
5. Реализуйте отображение всех новых блюд на страницу с помощью JavaScript. Проверьте, что скрипт работает для всех категорий.
6. Создайте скрипт, с помощью которого можно фильтровать отображаемые в каждой категории блюда.
- При клике на ссылку с фильтром, в блоке отображаются только блюда с соответствующим значением kind. Например, пользователь выбрал фильтр "мясное" в категории с главными блюдами.
- Если затем пользователь выбрал другой фильтр, блюда должны измениться. Например, поменяем фильтр на "рыбное"
- При клике на ссылку, ей добавляется класс "active" и меняется стиль ее отображения.
- Если кликнуть по выбранному фильтру повторно, класс "active" удаляется, а в блоке снова отображаются все блюда.
- Если ни один из фильтров не был выбран, в блоке отображаются все блюда.
Функции из предыдущей лабораторной должны работать корректно, в том числе после фильтрации блюд.
### Материалы для изучения
#### HTML
[Атрибуты data-* [Doka]](https://doka.guide/html/data-attributes/)
#### JS
[Функция [Doka]](https://doka.guide/js/function/)
[Функции [MDN]](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Functions)
[push() [Doka]](https://doka.guide/js/array-push/)
[pop() [Doka]](https://doka.guide/js/array-pop/)
[unshift() [Doka]](https://doka.guide/js/array-unshift/)
[shift() [Doka]](https://doka.guide/js/array-shift/)
[slice() [Doka]](https://doka.guide/js/array-slice/)
[splice() [Doka]](https://doka.guide/js/array-splice/)
[includes() [Doka]](https://doka.guide/js/includes/)
[classList [Doka]](https://doka.guide/js/element-classlist/)
[Области видимости [Doka]](https://doka.guide/js/closures/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

546
labs/lab-05/styles.css Normal file
View file

@ -0,0 +1,546 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: 'Montserrat', sans-serif;
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
background-color: #2d5016;
color: white;
padding: 20px 40px;
margin: 0;
}
h1 {
margin: 0 0 15px 0;
font-size: 36px;
color: white;
}
nav {
margin: 0;
padding: 10px 0;
display: flex;
flex-wrap: wrap;
gap: 15px;
}
nav a {
text-decoration: none;
color: white;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
}
nav a:hover {
color: #a8d08d;
}
nav a#active {
color: tomato;
}
main {
margin: 0 auto;
padding: 40px 60px;
max-width: 1200px;
flex: 1;
}
section {
margin: 0 0 50px 0;
padding: 30px;
background-color: #f9f9f9;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.section-header h2 {
margin: 0;
}
h2 {
font-size: 28px;
color: #2d5016;
margin: 0 0 20px 0;
}
p {
font-family: 'Montserrat', sans-serif;
font-size: 16px;
color: #333;
line-height: 1.6;
margin: 0 0 15px 0;
}
img {
width: 100%;
height: 400px;
object-fit: cover;
margin: 0 0 20px 0;
}
ul {
margin: 20px 0;
padding: 0 0 0 25px;
}
ul li {
font-family: 'Montserrat', sans-serif;
font-size: 16px;
color: #333;
margin: 0 0 12px 0;
line-height: 1.5;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
table th {
background-color: #2d5016;
color: white;
padding: 15px;
text-align: left;
border: 1px solid #2d5016;
font-size: 16px;
}
table td {
padding: 12px 15px;
border: 1px solid #ddd;
text-align: left;
font-size: 15px;
color: #333;
}
table tr:nth-child(even) {
background-color: #f2f2f2;
}
.filter-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-btn {
background-color: white;
border: 2px solid #2d5016;
color: #2d5016;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
}
.filter-btn:hover {
background-color: #f1eee9;
}
.filter-btn.active {
background-color: #2d5016;
color: white;
}
.dishes-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.dish-card {
display: flex;
flex-direction: column;
padding: 30px 40px;
border-radius: 35px;
cursor: pointer;
filter: drop-shadow(17px 19px 24px rgba(0, 0, 0, 0.13));
background-color: white;
}
.dish-card:hover {
border: 2px solid tomato;
}
.dish-card:hover button {
background-color: tomato;
color: white;
}
.dish-card img {
width: 100%;
height: auto;
aspect-ratio: 12 / 9;
object-fit: cover;
border-radius: 35px;
margin: 0 0 15px 0;
}
.dish-price {
font-size: 20px;
font-weight: 600;
margin: 0 0 10px 0;
}
.dish-name {
font-size: 18px;
font-weight: 600;
margin: 0 0 10px 0;
}
.dish-weight {
color: #888;
margin: 0 0 10px 0;
margin-top: auto;
}
.dish-card button {
background-color: #f1eee9;
border: none;
padding: 10px 30px;
border-radius: 10px;
cursor: pointer;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
margin-top: 10px;
}
.order-form {
margin: 0 auto;
padding: 40px 60px;
max-width: 1200px;
background-color: white;
}
.order-form h2 {
text-align: center;
margin-bottom: 30px;
}
#order-summary {
background-color: #f9f9f9;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.empty-order {
text-align: center;
color: #888;
font-size: 18px;
}
.order-category {
margin-bottom: 25px;
}
.order-category h3 {
font-size: 20px;
color: #2d5016;
margin: 0 0 10px 0;
}
.order-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
font-size: 16px;
}
.order-item span:first-child {
font-weight: 600;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 70%;
}
.order-item span:last-child {
color: #2d5016;
font-weight: 600;
white-space: nowrap;
}
.not-selected {
color: #888;
font-style: italic;
margin: 5px 0;
}
.order-total {
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid #2d5016;
}
.order-total h3 {
font-size: 22px;
color: #2d5016;
margin: 0 0 10px 0;
}
.total-price {
font-size: 24px;
font-weight: 700;
color: #2d5016;
margin: 0;
}
.form-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.order-section {
display: flex;
flex-direction: column;
}
.order-section h3 {
font-size: 22px;
color: #2d5016;
margin: 0 0 20px 0;
}
.customer-section {
display: flex;
flex-direction: column;
}
.customer-section h3 {
font-size: 22px;
color: #2d5016;
margin: 0 0 20px 0;
}
.order-form label {
font-size: 16px;
color: #333;
margin: 0 0 8px 0;
font-weight: 600;
}
.order-form select,
.order-form input[type="text"],
.order-form input[type="email"],
.order-form input[type="tel"],
.order-form input[type="time"],
.order-form textarea {
width: 100%;
padding: 12px 15px;
margin: 0 0 20px 0;
border: 1px solid #ddd;
border-radius: 8px;
font-family: 'Montserrat', sans-serif;
font-size: 15px;
}
.order-form select {
cursor: pointer;
background-color: white;
}
.order-form textarea {
height: 100px;
resize: vertical;
}
.checkbox-group {
display: flex;
align-items: center;
margin: 0 0 20px 0;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0 10px 0 0;
cursor: pointer;
}
.checkbox-group label {
margin: 0;
font-weight: 400;
cursor: pointer;
}
.radio-group {
margin: 0 0 20px 0;
}
.radio-group > label {
display: block;
margin-bottom: 10px;
}
.radio-group > div {
display: flex;
align-items: center;
margin: 0 0 8px 0;
}
.radio-group input[type="radio"] {
width: 18px;
height: 18px;
margin: 0 10px 0 0;
cursor: pointer;
}
.radio-group input[type="radio"] + label {
margin: 0;
font-weight: 400;
cursor: pointer;
}
.form-buttons {
display: flex;
gap: 15px;
margin-top: 10px;
}
.form-buttons button {
flex: 1;
padding: 15px 30px;
border: none;
border-radius: 10px;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s;
}
.form-buttons button[type="reset"] {
background-color: #f1eee9;
color: #333;
}
.form-buttons button[type="reset"]:hover {
background-color: #e0ddd8;
}
.form-buttons button[type="submit"] {
background-color: #2d5016;
color: white;
}
.form-buttons button[type="submit"]:hover {
background-color: #3d6020;
}
.form-hint {
display: block;
font-size: 13px;
color: #666;
margin: -15px 0 20px 0;
line-height: 1.4;
}
footer {
background-color: black;
color: white;
padding: 30px 60px;
margin: 0;
}
footer p {
color: white;
margin: 0 0 10px 0;
}
footer a {
text-decoration: none;
color: white;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
}
footer a:hover {
color: #a8d08d;
}
@media (max-width: 800px) {
.dishes-grid {
grid-template-columns: 1fr 1fr;
}
nav {
justify-content: space-between;
}
.about-company img {
width: 500px;
}
.form-container {
grid-template-columns: 1fr;
gap: 20px;
}
}
@media (max-width: 600px) {
h1 {
text-align: center;
}
nav {
flex-direction: column;
align-items: center;
}
nav a {
font-size: 20px;
}
section h2 {
text-align: center;
}
.dishes-grid {
grid-template-columns: 1fr;
}
.about-company img {
width: 100%;
}
table th,
table td {
font-size: 14px;
}
.form-container {
grid-template-columns: 1fr;
}
.form-buttons {
flex-direction: column;
}
}
@media (max-width: 400px) {
table th,
table td {
font-size: 12px;
}
}