This commit is contained in:
Egor Deev 2025-11-20 15:44:03 +03:00 committed by GitHub
parent be90a66a9a
commit 6b1e460d7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2110 additions and 0 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

102
labs/lab-08/index.html Normal file
View file

@ -0,0 +1,102 @@
<!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/styles.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="index.html" id="active">Главная</a>
<a href="templates/menu.html">Собрать ланч</a>
<a href="templates/order.html">Оформить заказ</a>
<a href="templates/delivery.html">Доставка</a>
<a href="templates/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>Режим работы: Пн-Пт с 7:00 до 23:00</p>
</footer>
</body>
</html>

314
labs/lab-08/js/menu.js Normal file
View file

@ -0,0 +1,314 @@
let dishes = [];
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 loadDishes() {
const apiUrl = 'https://edu.std-900.ist.mospolytech.ru/labs/api/dishes';
console.log('Загрузка блюд из API...');
fetch(apiUrl)
.then(function(response) {
console.log('Ответ получен:', response.status);
if (!response.ok) {
throw new Error('Ошибка загрузки данных: ' + response.status);
}
return response.json();
})
.then(function(data) {
console.log('Данные загружены:', data.length, 'блюд');
dishes = data;
sortDishes();
loadOrderFromLocalStorage();
displayDishes();
updateOrderPanel();
})
.catch(function(error) {
console.error('Ошибка при загрузке блюд:', error);
alert('Не удалось загрузить меню. Проверьте подключение к интернету.');
});
}
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');
if (!soupSection || !mainCourseSection || !saladSection ||
!drinkSection || !dessertSection) {
console.error('Не найдены секции для отображения блюд');
return;
}
soupSection.innerHTML = '';
mainCourseSection.innerHTML = '';
saladSection.innerHTML = '';
drinkSection.innerHTML = '';
dessertSection.innerHTML = '';
console.log('Отображение блюд. Всего:', dishes.length);
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();
restoreSelection();
}
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) {
const previousDish = selectedDishes[dish.category];
if (previousDish) {
const previousCard = document.querySelector(
`.dish-card[data-dish="${previousDish.keyword}"]`
);
if (previousCard) {
previousCard.classList.remove('selected');
}
}
selectedDishes[dish.category] = dish;
const currentCard = document.querySelector(
`.dish-card[data-dish="${dish.keyword}"]`
);
if (currentCard) {
currentCard.classList.add('selected');
}
saveOrderToLocalStorage();
updateOrderPanel();
}
function saveOrderToLocalStorage() {
const order = {};
Object.keys(selectedDishes).forEach(function(category) {
if (selectedDishes[category]) {
order[category] = selectedDishes[category].id;
}
});
localStorage.setItem('selectedDishes', JSON.stringify(order));
console.log('Заказ сохранен в localStorage:', order);
}
function loadOrderFromLocalStorage() {
const savedOrder = localStorage.getItem('selectedDishes');
if (!savedOrder) {
console.log('Сохраненный заказ не найден');
return;
}
try {
const order = JSON.parse(savedOrder);
console.log('Загружен заказ из localStorage:', order);
Object.keys(order).forEach(function(category) {
const dishId = order[category];
const dish = dishes.find(function(d) {
return d.id === dishId;
});
if (dish) {
selectedDishes[category] = dish;
}
});
} catch (error) {
console.error('Ошибка при загрузке заказа:', error);
}
}
function updateOrderPanel() {
const orderPanel = document.getElementById('order-panel');
const orderPanelPrice = document.getElementById('order-panel-price');
const orderPanelLink = document.getElementById('order-panel-link');
if (!orderPanel || !orderPanelPrice || !orderPanelLink) {
console.error('Не найдены элементы панели заказа');
return;
}
const hasSelection = selectedDishes.soup ||
selectedDishes['main-course'] ||
selectedDishes.salad ||
selectedDishes.drink ||
selectedDishes.dessert;
if (!hasSelection) {
orderPanel.classList.add('hidden');
return;
}
orderPanel.classList.remove('hidden');
let totalPrice = 0;
Object.keys(selectedDishes).forEach(function(category) {
if (selectedDishes[category]) {
totalPrice += selectedDishes[category].price;
}
});
orderPanelPrice.textContent = totalPrice + ' руб.';
if (isValidCombo()) {
orderPanelLink.classList.remove('disabled');
} else {
orderPanelLink.classList.add('disabled');
}
}
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();
});
});
}
function isValidCombo() {
const hasSoup = selectedDishes.soup !== null;
const hasMainCourse = selectedDishes['main-course'] !== null;
const hasSalad = selectedDishes.salad !== null;
const hasDrink = selectedDishes.drink !== null;
if (hasSoup && hasMainCourse && hasSalad && hasDrink) {
return true;
}
if (hasSoup && hasMainCourse && hasDrink) {
return true;
}
if (hasSoup && hasSalad && hasDrink) {
return true;
}
if (hasMainCourse && hasSalad && hasDrink) {
return true;
}
if (hasMainCourse && hasDrink) {
return true;
}
return false;
}
function restoreSelection() {
Object.keys(selectedDishes).forEach(function(category) {
const dish = selectedDishes[category];
if (dish) {
const card = document.querySelector(
`.dish-card[data-dish="${dish.keyword}"]`
);
if (card) {
card.classList.add('selected');
}
}
});
}
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM загружен, начинаем инициализацию');
loadDishes();
setupFilters();
});

448
labs/lab-08/js/order.js Normal file
View file

@ -0,0 +1,448 @@
const API_URL = 'https://edu.std-900.ist.mospolytech.ru/labs/api';
const API_KEY = '358a63a5-52ae-4ab0-800b-90f75ce5a5c2';
let dishes = [];
let orderDishes = {
soup: null,
'main-course': null,
salad: null,
drink: null,
dessert: null
};
function loadDishes() {
console.log('Загрузка блюд для страницы заказа...');
fetch(API_URL + '/dishes')
.then(function(response) {
console.log('Ответ получен:', response.status);
if (!response.ok) {
throw new Error('Ошибка загрузки данных: ' + response.status);
}
return response.json();
})
.then(function(data) {
console.log('Данные загружены:', data.length, 'блюд');
dishes = data;
loadOrderFromLocalStorage();
displayOrderDishes();
updateOrderSummary();
})
.catch(function(error) {
console.error('Ошибка при загрузке блюд:', error);
alert('Не удалось загрузить данные о блюдах. Проверьте подключение.');
});
}
function loadOrderFromLocalStorage() {
const savedOrder = localStorage.getItem('selectedDishes');
if (!savedOrder) {
console.log('Сохраненный заказ не найден');
return;
}
try {
const order = JSON.parse(savedOrder);
console.log('Загружен заказ из localStorage:', order);
Object.keys(order).forEach(function(category) {
const dishId = order[category];
const dish = dishes.find(function(d) {
return d.id === dishId;
});
if (dish) {
orderDishes[category] = dish;
console.log('Найдено блюдо:', dish.name, 'для категории:', category);
} else {
console.warn('Блюдо с ID', dishId, 'не найдено для категории:', category);
}
});
} catch (error) {
console.error('Ошибка при загрузке заказа:', error);
}
}
function displayOrderDishes() {
const orderDishesContainer = document.getElementById('order-dishes');
const emptyOrderMessage = document.getElementById('empty-order');
if (!orderDishesContainer || !emptyOrderMessage) {
console.error('Не найдены контейнеры для отображения заказа');
return;
}
const hasAnyDish = Object.values(orderDishes).some(function(dish) {
return dish !== null;
});
console.log('Есть блюда для отображения:', hasAnyDish);
if (!hasAnyDish) {
orderDishesContainer.innerHTML = '';
emptyOrderMessage.classList.remove('hidden');
return;
}
emptyOrderMessage.classList.add('hidden');
orderDishesContainer.innerHTML = '';
Object.keys(orderDishes).forEach(function(category) {
const dish = orderDishes[category];
if (dish) {
const dishCard = createOrderDishCard(dish);
orderDishesContainer.insertAdjacentHTML('beforeend', dishCard);
}
});
addRemoveHandlers();
}
function createOrderDishCard(dish) {
return `
<div class="dish-card" data-dish-id="${dish.id}">
<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 class="remove-dish-btn">Удалить</button>
</div>
`;
}
function addRemoveHandlers() {
const removeButtons = document.querySelectorAll('.remove-dish-btn');
removeButtons.forEach(function(button) {
button.addEventListener('click', function(event) {
event.stopPropagation();
const card = this.closest('.dish-card');
const dishId = parseInt(card.dataset.dishId);
removeDishFromOrder(dishId);
});
});
}
function removeDishFromOrder(dishId) {
console.log('Удаление блюда с ID:', dishId);
Object.keys(orderDishes).forEach(function(category) {
if (orderDishes[category] && orderDishes[category].id === dishId) {
orderDishes[category] = null;
}
});
saveOrderToLocalStorage();
displayOrderDishes();
updateOrderSummary();
}
function saveOrderToLocalStorage() {
const order = {};
Object.keys(orderDishes).forEach(function(category) {
if (orderDishes[category]) {
order[category] = orderDishes[category].id;
}
});
localStorage.setItem('selectedDishes', JSON.stringify(order));
console.log('Заказ сохранен в localStorage:', order);
}
function updateOrderSummary() {
const orderSummary = document.getElementById('order-summary');
if (!orderSummary) {
console.error('Не найден контейнер для сводки заказа');
return;
}
const hasSelection = Object.values(orderDishes).some(function(dish) {
return dish !== null;
});
if (!hasSelection) {
orderSummary.innerHTML =
'<p class="empty-order">Ничего не выбрано</p>';
return;
}
let summaryHTML = '';
let totalPrice = 0;
if (orderDishes.soup) {
summaryHTML += `
<div class="order-category">
<h3>Суп</h3>
<div class="order-item">
<span>${orderDishes.soup.name}</span>
<span>${orderDishes.soup.price} руб.</span>
</div>
</div>
`;
totalPrice += orderDishes.soup.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Суп</h3>
<p class="not-selected">Блюдо не выбрано</p>
</div>
`;
}
if (orderDishes['main-course']) {
summaryHTML += `
<div class="order-category">
<h3>Главное блюдо</h3>
<div class="order-item">
<span>${orderDishes['main-course'].name}</span>
<span>${orderDishes['main-course'].price} руб.</span>
</div>
</div>
`;
totalPrice += orderDishes['main-course'].price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Главное блюдо</h3>
<p class="not-selected">Блюдо не выбрано</p>
</div>
`;
}
if (orderDishes.salad) {
summaryHTML += `
<div class="order-category">
<h3>Салат</h3>
<div class="order-item">
<span>${orderDishes.salad.name}</span>
<span>${orderDishes.salad.price} руб.</span>
</div>
</div>
`;
totalPrice += orderDishes.salad.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Салат</h3>
<p class="not-selected">Блюдо не выбрано</p>
</div>
`;
}
if (orderDishes.drink) {
summaryHTML += `
<div class="order-category">
<h3>Напиток</h3>
<div class="order-item">
<span>${orderDishes.drink.name}</span>
<span>${orderDishes.drink.price} руб.</span>
</div>
</div>
`;
totalPrice += orderDishes.drink.price;
} else {
summaryHTML += `
<div class="order-category">
<h3>Напиток</h3>
<p class="not-selected">Напиток не выбран</p>
</div>
`;
}
if (orderDishes.dessert) {
summaryHTML += `
<div class="order-category">
<h3>Десерт</h3>
<div class="order-item">
<span>${orderDishes.dessert.name}</span>
<span>${orderDishes.dessert.price} руб.</span>
</div>
</div>
`;
totalPrice += orderDishes.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 isValidCombo() {
const hasSoup = orderDishes.soup !== null;
const hasMainCourse = orderDishes['main-course'] !== null;
const hasSalad = orderDishes.salad !== null;
const hasDrink = orderDishes.drink !== null;
if (hasSoup && hasMainCourse && hasSalad && hasDrink) {
return true;
}
if (hasSoup && hasMainCourse && hasDrink) {
return true;
}
if (hasSoup && hasSalad && hasDrink) {
return true;
}
if (hasMainCourse && hasSalad && hasDrink) {
return true;
}
if (hasMainCourse && hasDrink) {
return true;
}
return false;
}
function getValidationMessage() {
const hasSoup = orderDishes.soup !== null;
const hasMainCourse = orderDishes['main-course'] !== null;
const hasSalad = orderDishes.salad !== null;
const hasDrink = orderDishes.drink !== null;
if (!hasSoup && !hasMainCourse && !hasSalad && !hasDrink) {
return 'Ничего не выбрано. Выберите блюда для заказа';
}
if (!hasDrink) {
return 'Выберите напиток';
}
if (hasSoup && !hasMainCourse && !hasSalad) {
return 'Выберите главное блюдо/салат/стартер';
}
if (hasSalad && !hasSoup && !hasMainCourse) {
return 'Выберите суп или главное блюдо';
}
if (!hasMainCourse && !hasSoup && (hasDrink || orderDishes.dessert)) {
return 'Выберите главное блюдо';
}
return '';
}
function setupFormSubmission() {
const form = document.getElementById('order-form');
if (!form) {
console.error('Форма заказа не найдена');
return;
}
form.addEventListener('submit', function(event) {
event.preventDefault();
console.log('Отправка формы заказа');
if (!isValidCombo()) {
const message = getValidationMessage();
alert(message);
return;
}
const formData = new FormData(form);
const orderData = {
full_name: formData.get('full_name'),
email: formData.get('email'),
subscribe: formData.get('subscribe') === '1' ? 1 : 0,
phone: formData.get('phone'),
delivery_address: formData.get('delivery_address'),
delivery_type: formData.get('delivery_type'),
delivery_time: formData.get('delivery_time') || '',
comment: formData.get('comment') || ''
};
if (orderDishes.soup) {
orderData.soup_id = orderDishes.soup.id;
}
if (orderDishes['main-course']) {
orderData.main_course_id = orderDishes['main-course'].id;
}
if (orderDishes.salad) {
orderData.salad_id = orderDishes.salad.id;
}
if (orderDishes.drink) {
orderData.drink_id = orderDishes.drink.id;
}
if (orderDishes.dessert) {
orderData.dessert_id = orderDishes.dessert.id;
}
console.log('Данные заказа:', orderData);
sendOrder(orderData);
});
}
function sendOrder(orderData) {
const url = API_URL + '/orders?api_key=' + API_KEY;
console.log('Отправка заказа на сервер:', url);
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(orderData)
})
.then(function(response) {
console.log('Ответ от сервера:', response.status);
if (!response.ok) {
return response.json().then(function(errorData) {
throw new Error(errorData.error ||
'Ошибка при оформлении заказа');
});
}
return response.json();
})
.then(function(data) {
console.log('Заказ успешно создан:', data);
alert('Заказ успешно оформлен! Номер заказа: ' + data.id);
localStorage.removeItem('selectedDishes');
window.location.href = '../index.html';
})
.catch(function(error) {
console.error('Ошибка при отправке заказа:', error);
alert('Не удалось оформить заказ: ' + error.message);
});
}
function setupResetButton() {
const resetButton = document.getElementById('reset-button');
if (!resetButton) {
console.error('Кнопка сброса не найдена');
return;
}
resetButton.addEventListener('click', function() {
const form = document.getElementById('order-form');
form.reset();
});
}
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM загружен для страницы заказа');
loadDishes();
setupFormSubmission();
setupResetButton();
});

View file

@ -0,0 +1,19 @@
Local storage
1. Что такое localStorage?
2. Какие ограничения имеет localStorage по объему хранимой информации?
3. Как добавить данные в localStorage с помощью JavaScript?
4. Можно ли хранить объекты в localStorage напрямую? Если нет, то как это сделать?
5. Какой метод используется для удаления конкретного ключа из localStorage?
6. Чем отличается localStorage от sessionStorage?
7. Может ли localStorage быть использован для хранения конфиденциальных данных? Почему да/почему нет?
8. Назовите преимущества и недостатки использования localStorage.
9. Есть ли возможность очистки всего содержимого localStorage одним вызовом метода?
10. Когда данные в localStorage могут быть автоматически удалены?
Взаимодействие клиента и сервера
1. Что такое клиент-серверная архитектура? В чем заключается ее суть и какие компоненты она включает?
2. Какие протоколы передачи данных используются в веб-разработке для взаимодействия клиента с сервером? Перечислите основные протоколы и объясните их назначение.
3. Что такое RESTful API? Дайте определение и приведите пример реализации простого RESTful сервиса.
4. Объясните принцип работы сессий и куков. Как они помогают сохранять состояние пользователя на стороне сервера и клиента соответственно?
5. Как осуществляется аутентификация и авторизация на уровне сервера? Описать подходы к защите доступа к ресурсам и методы обеспечения безопасности данных.

View file

@ -0,0 +1,91 @@
# Лабораторная работа № 8. Реализация функциональности оформления заказа
---
Реализуйте сохранение выбранных пользователем блюд, чтобы после обновления страницы выбор пользователя не сбрасывался. Добавьте страницу оформления заказа, где будет отображаться текущий состав заказа и форма ввода данных. Реализуйте возможность оформления заказа отправку данных на сервер.
## Порядок выполнения
1. Реализуйте сохранение данных о выбранных пользователем блюдах в localStorage, чтобы в последующем иметь возможность получить эти данные на странице "Оформить заказ". В localStorage должны храниться только идентификаторы выбранных блюд. Формат хранения данных выбирается на усмотрение студента.
2. Добавьте страницу "Оформить заказ". Внешний вид страницы представлен на макете ниже. Добавьте ссылку на эту страницу в навигационное меню. На странице "Оформить заказ" должно быть два раздела: "Состав заказа" и "Оформление заказа".
- В разделе "Состав заказа" должны быть отображены ранее добавленные пользователем в заказ блюда (список выбранных блюд нужно загрузить из localStorage, а данные для отображения загрузить с сервера).
- Формат отображения блюд такой же, как на странице "Собрать ланч".
- Вместо кнопки "Добавить" в карточке блюда должна быть кнопка "Удалить", по нажатию на которую соответствующее блюдо удаляется из заказа (т. е. информация о нём удаляется из localStorage) и карточка удаляется со страницы.
- Если ни одно блюдо не выбрано, нужно отобразить пользователю текст "Ничего не выбрано. Чтобы добавить блюда в заказ, перейдите на страницу Собрать ланч.", где текст "Собрать ланч" гиперссылка на соответствующую страницу.
- В разделе "Оформление заказа" должна отображаться форма оформления заказа, перенесённая со страницы "Собрать ланч".
- Выбранные пользователем блюда отображаются в левой части формы (см. макет выше), напротив каждого блюда отображается его стоимость.
- При удалении блюда из заказа в соответствующем ему пункте должен отобразиться текст "Не выбран" (или "Не выбрано" для "Главного блюда") и должна быть пересчитана итоговая стоимость заказа.
- Перед отправкой данных формы на сервер должна производиться проверка соответствия состава заказа одному из доступных комбо (см. задание к ЛР 6).
3. Внесите исправления в страницу "Собрать ланч": вместо формы оформления заказа разместите панель для перехода к оформлению заказа, в которой будет отображаться текущая стоимость добавленных в заказ блюд и ссылка "Перейти к оформлению", ведущая на страницу оформления заказа.
- Панель для перехода к оформлению заказа должна быть скрыта, если пользователь не добавил в заказ ни одного блюда.
- Ссылка "Перейти к оформлению" должна быть недоступна до тех пор, пока сосав заказа не удовлетворяет одному из доступных комбо (см. задание к ЛР 6).
- Для размещения элемента на странице используйте position: sticky.
- При добавлении очередного блюда в заказ, стоимость должна пересчитываться.
- При открытии страницы "Собрать ланч" должны учитываться данные текущего заказа, хранящиеся в localStorage (т. е. в меню должны быть выделены выбранные блюда, рассчитана и отображена текущая суммарная стоимость выбранных блюд и т. д.).
4. Реализуйте отправку данных заказа на сервер по нажатию на кнопку "Отправить".
- Запрос должен быть отправлен при помощи fetch.
- В случае возникновения ошибки при отправке запроса или его обработке на стороне сервера пользователю должно быть отображено уведомление об ошибке (можно использовать функцию alert или написать кастомное модальное окно на ваше усмотрение).
- После успешного оформления заказа данные о выбранных блюдах из localStorage должны удаляться. Если не удалось оформить заказ данные не удаляются.
## Инструкция по работе с API
Для получения доступа к API необходимо пройти процедуру авторизации. Для авторизации нужно указать в качестве параметра запроса api_key значение уникального ключа, который выдаётся каждому пользователю. Ключ представляет собой идентификатор UUIDv4, который является случайным 16-байтным номером (например, 123e4567-e89b-12d3-a456-426655440000).
**Обратите внимание, что параметр api_key всегда передаётся в строке запроса.**
Пользователь может просматривать, редактировать и удалять только свои заказы. В один момент времени в базе данных может быть не более 10 заказов, созданных одним и тем же пользователем.
Если пользователь попробует совершить действие, не пройдя авторизацию, в качестве ответа на его запрос придёт сообщение:
{"error": "Для получения доступа к API необходимо пройти процедуру авторизации. Для этого нужно передать в запросе персональный API Key."}
При передаче параметров в POST- и PUT-запросах данные должны передаваться в теле запроса в формате application/json (нужно вручную сериализовать данные и установить значение заголовка Content-Type) или multipart/form-data (достаточно при отправке данных формы воспользоваться объектом FormData).
При оформлении заказа в запросе к серверу должны быть указаны значения обязательных полей:
|Название|Тип|Обязательное|Только для чтения|Примечание|
|---|---|---|---|---|
|id|Integer||Да|Устанавливается сервером.|
|full_name|String|Да|||
|email|String|Да||
|subscribe|Boolean|Нет||Допустимы значения 0 и 1.|
|phone|String|Да|||
|delivery_address|String|Да|||
|delivery_type|String|Да||Допустимы значения "now" и "by_time".|
|delivery_time|Time|Нет||Значение передаётся в формате HH:MM. <br>Доступное время доставки: с 7:00 до 23:00 с шагом 5 минут. <br>Не должно быть пустым, если delivery_type=by_time. <br>Не должно быть раньше текущего времени.|
|comment|String|Нет|||
|soup_id|Integer|Нет||Обязательность поля зависит от состава комбо.|
|main_course_id|Integer|Нет||Обязательность поля зависит от состава комбо.|
|salad_id|Integer|Нет||Обязательность поля зависит от состава комбо.|
|drink_id|Integer|Да|||
|dessert_id|Integer|Нет|||
|created_at|DateTime||Да|Устанавливается сервером.|
|updated_at|DateTime||Да|Устанавливается сервером.|
|student_id|Integer||Да|Устанавливается сервером.|
Доступные пользователю действия представлены в таблице:
|Действие|Метод и путь|Формат ответа|Примечание|
|---|---|---|---|
|Получить данные всех блюд|GET /labs/api/dishes|JSON [{item1},{item2},...{itemN}]||
|Получить данные конкретного блюда|GET /labs/api/dishes/{int:dish_id}|JSON {Item}|Вместо {int:dish_id} нужно подставить целое число идентификатор блюда.|
|Получить данные всех заказов|GET /labs/api/orders|JSON [{item1},{item2},...{itemN}]||
|Получить данные конкретного заказа|GET /labs/api/orders/{int:order_id}|JSON {Item}|Вместо {int:order_id} нужно подставить целое число идентификатор заказа.|
|Создать новый заказ|POST /labs/api/orders|JSON {newItem}|Нужно передать значения всех обязательных полей.|
|Изменить заказ|PUT /labs/api/orders/{int:order_id}|JSON {updateItem}|Вместо {int:order_id} нужно подставить целое число идентификатор заказа. <br>Достаточно передать только значения изменившихся полей.|
|Удалить заказ|DELETE /labs/api/orders/{int:order_id}|JSON {Item}|Вместо {int:order_id} нужно подставить целое число идентификатор заказа.|
### Материалы для изучения
[localStorage [Doka]](https://doka.guide/js/local-storage/)
[Получение данных с сервера [MDN]](https://developer.mozilla.org/ru/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data)
[Работа с JSON [MDN]](https://developer.mozilla.org/ru/docs/Learn/JavaScript/Objects/JSON)
[Промисы, async/await [Learn JS]](https://learn.javascript.ru/async)
[async/await [Doka]](https://doka.guide/js/async-await/)
[Использование промисов [MDN]](https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Using_promises)
[Fetch [Learn JS]](https://learn.javascript.ru/fetch)
[Fetch API [MDN]](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
[fetch() [Doka]](https://doka.guide/js/fetch/)
[Cross-Origin Resource Sharing (CORS) [MDN]](https://developer.mozilla.org/ru/docs/Web/HTTP/CORS)

290
labs/lab-08/styles/menu.css Normal file
View file

@ -0,0 +1,290 @@
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.section-header h2 {
margin: 0;
}
.combo-section {
margin-bottom: 50px;
}
.combo-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.combo-card {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 20px;
background-color: white;
padding: 20px;
border-radius: 15px;
}
.combo-dessert {
flex-direction: column;
gap: 10px;
}
.combo-item {
display: flex;
flex-direction: column;
align-items: center;
}
.combo-icon {
font-size: 50px;
display: block;
margin: 0 0 8px 0;
transition: transform 0.3s;
}
.combo-item:hover .combo-icon {
transform: translateY(-5px) scale(1.1);
}
.combo-item p {
margin: 0;
font-size: 14px;
text-align: center;
}
.combo-note-inline {
font-size: 12px;
text-align: center;
color: #666;
margin: 0;
font-style: italic;
}
.combo-note {
font-size: 14px;
color: #666;
font-style: italic;
text-align: center;
margin: 10px 0 0 0;
}
.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.selected {
border: 3px solid #2d5016;
background-color: #f0f7ec;
}
.dish-card.selected button {
background-color: #2d5016;
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;
}
.remove-dish-btn {
background-color: tomato;
color: white;
}
.remove-dish-btn:hover {
background-color: #ff4500;
}
.order-panel {
position: sticky;
bottom: 20px;
background-color: white;
padding: 20px 40px;
margin: 0 auto;
max-width: 1200px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
z-index: 100;
}
.order-panel.hidden {
display: none;
}
.order-panel-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.order-panel-info {
display: flex;
align-items: center;
gap: 15px;
}
.order-panel-label {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.order-panel-price {
font-size: 24px;
font-weight: 700;
color: #2d5016;
margin: 0;
}
.order-panel-button {
background-color: #2d5016;
color: white;
text-decoration: none;
padding: 15px 40px;
border-radius: 10px;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
}
.order-panel-button:hover {
background-color: #3d6020;
}
.order-panel-button.disabled {
background-color: #ccc;
color: #888;
cursor: not-allowed;
pointer-events: none;
}
@media (max-width: 800px) {
.combo-grid {
grid-template-columns: 1fr 1fr;
}
.dishes-grid {
grid-template-columns: 1fr 1fr;
}
.order-panel-content {
flex-direction: column;
}
}
@media (max-width: 600px) {
.combo-grid {
grid-template-columns: 1fr;
}
.dishes-grid {
grid-template-columns: 1fr;
}
.order-panel {
padding: 15px 20px;
}
.order-panel-content {
flex-direction: column;
align-items: stretch;
}
.order-panel-button {
text-align: center;
}
}

View file

@ -0,0 +1,266 @@
.empty-order-message {
text-align: center;
padding: 40px 20px;
}
.empty-order-message.hidden {
display: none;
}
.empty-order-message p {
font-size: 18px;
color: #666;
}
.empty-order-message a {
color: #2d5016;
text-decoration: underline;
}
.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="button"] {
background-color: #f1eee9;
color: #333;
}
.form-buttons button[type="button"]: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;
}
@media (max-width: 800px) {
.form-container {
grid-template-columns: 1fr;
gap: 20px;
}
}
@media (max-width: 600px) {
.form-container {
grid-template-columns: 1fr;
}
.form-buttons {
flex-direction: column;
}
}

View file

@ -0,0 +1,190 @@
* {
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;
}
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;
}
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) {
nav {
justify-content: space-between;
}
.about-company img {
width: 500px;
}
}
@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;
}
.about-company img {
width: 100%;
}
table th,
table td {
font-size: 14px;
}
}
@media (max-width: 400px) {
table th,
table td {
font-size: 12px;
}
}

View file

@ -0,0 +1,42 @@
<!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/styles.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="../index.html">Главная</a>
<a href="menu.html">Собрать ланч</a>
<a href="order.html">Оформить заказ</a>
<a href="delivery.html">Доставка</a>
<a href="about.html" id="active">О нас</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main>
<section>
<h2>О нашей компании</h2>
<p>ЭкоЛанч - это команда профессионалов, которые заботятся о вашем здоровье и комфорте. Мы готовим вкусные и полезные блюда из свежих продуктов и доставляем их прямо в ваш офис.</p>
<p>Наша миссия - сделать здоровое питание доступным и удобным для каждого занятого человека в Москве.</p>
</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>Режим работы: Пн-Пт с 7:00 до 23:00</p>
</footer>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!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/styles.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="../index.html">Главная</a>
<a href="menu.html">Собрать ланч</a>
<a href="order.html">Оформить заказ</a>
<a href="delivery.html" id="active">Доставка</a>
<a href="about.html">О нас</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main>
<section>
<h2>Условия доставки</h2>
<p>Мы осуществляем доставку здоровых бизнес-ланчей по всей Москве. Доставка производится с понедельника по пятницу с 7:00 до 23:00.</p>
<p>Доставка по всей Москве в пределах МКАД осуществляется бесплатно!</p>
</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>Режим работы: Пн-Пт с 7:00 до 23:00</p>
</footer>
</body>
</html>

View file

@ -0,0 +1,198 @@
<!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/styles.css">
<link rel="stylesheet" href="../styles/menu.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="../index.html">Главная</a>
<a href="menu.html" id="active">Собрать ланч</a>
<a href="order.html">Оформить заказ</a>
<a href="delivery.html">Доставка</a>
<a href="about.html">О нас</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main>
<section class="combo-section">
<h2>Доступные для заказа варианты ланча</h2>
<div class="combo-grid">
<div class="combo-card">
<div class="combo-item">
<span class="combo-icon">🍲</span>
<p>Суп</p>
</div>
<div class="combo-item">
<span class="combo-icon">🍽️</span>
<p>Главное блюдо</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥗</span>
<p>Салат</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥤</span>
<p>Напиток</p>
</div>
</div>
<div class="combo-card">
<div class="combo-item">
<span class="combo-icon">🍲</span>
<p>Суп</p>
</div>
<div class="combo-item">
<span class="combo-icon">🍽️</span>
<p>Главное блюдо</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥤</span>
<p>Напиток</p>
</div>
</div>
<div class="combo-card">
<div class="combo-item">
<span class="combo-icon">🍲</span>
<p>Суп</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥗</span>
<p>Салат</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥤</span>
<p>Напиток</p>
</div>
</div>
<div class="combo-card">
<div class="combo-item">
<span class="combo-icon">🍽️</span>
<p>Главное блюдо</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥗</span>
<p>Салат</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥤</span>
<p>Напиток</p>
</div>
</div>
<div class="combo-card">
<div class="combo-item">
<span class="combo-icon">🍽️</span>
<p>Главное блюдо</p>
</div>
<div class="combo-item">
<span class="combo-icon">🥤</span>
<p>Напиток</p>
</div>
</div>
<div class="combo-card combo-dessert">
<div class="combo-item">
<span class="combo-icon">🍰</span>
<p>Десерт</p>
</div>
<p class="combo-note-inline">(Можно добавить к любому заказу)</p>
</div>
</div>
<p class="combo-note">Десерты можно добавить к любому варианту ланча</p>
</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="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>
<div id="order-panel" class="order-panel hidden">
<div class="order-panel-content">
<div class="order-panel-info">
<p class="order-panel-label">Ваш заказ:</p>
<p class="order-panel-price" id="order-panel-price">0 руб.</p>
</div>
<a href="order.html" id="order-panel-link" class="order-panel-button disabled">
Перейти к оформлению
</a>
</div>
</div>
<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>Режим работы: Пн-Пт с 7:00 до 23:00</p>
</footer>
<script src="../js/menu.js"></script>
</body>
</html>

View file

@ -0,0 +1,108 @@
<!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/styles.css">
<link rel="stylesheet" href="../styles/menu.css">
<link rel="stylesheet" href="../styles/order.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="../index.html">Главная</a>
<a href="menu.html">Собрать ланч</a>
<a href="order.html" id="active">Оформить заказ</a>
<a href="delivery.html">Доставка</a>
<a href="about.html">О нас</a>
<a href="#contacts">Контакты</a>
</nav>
</header>
<main>
<section>
<h2>Состав заказа</h2>
<div id="order-dishes" class="dishes-grid"></div>
<div id="empty-order" class="empty-order-message hidden">
<p>Ничего не выбрано. Чтобы добавить блюда в заказ, перейдите на страницу <a href="menu.html">Собрать ланч</a>.</p>
</div>
</section>
<section class="order-form">
<h2>Оформление заказа</h2>
<form id="order-form" 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="full_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" value="1" 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="delivery_address" id="address" placeholder="Введите адрес доставки" required>
<small class="form-hint">Доставка осуществляется только по Москве</small>
<div class="radio-group">
<label>Время доставки</label>
<div>
<input type="radio" name="delivery_type" id="asap" value="now" required checked>
<label for="asap">Как можно скорее</label>
</div>
<div>
<input type="radio" name="delivery_type" id="specific-time" value="by_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="button" id="reset-button">Сбросить</button>
<button type="submit">Отправить заказ</button>
</div>
</div>
</div>
</form>
</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>Режим работы: Пн-Пт с 7:00 до 23:00</p>
</footer>
<script src="../js/order.js"></script>
</body>
</html>