mirror of
https://github.com/EDeev/web-tech.git
synced 2026-06-18 22:21:07 +03:00
LAB-08
This commit is contained in:
parent
be90a66a9a
commit
6b1e460d7a
13 changed files with 2110 additions and 0 deletions
BIN
labs/lab-08/img/main.jpg
Normal file
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
102
labs/lab-08/index.html
Normal 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
314
labs/lab-08/js/menu.js
Normal 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
448
labs/lab-08/js/order.js
Normal 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();
|
||||
});
|
||||
19
labs/lab-08/other/Вопросы для подготовки к защите ЛР №8.txt
Normal file
19
labs/lab-08/other/Вопросы для подготовки к защите ЛР №8.txt
Normal 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. Как осуществляется аутентификация и авторизация на уровне сервера? Описать подходы к защите доступа к ресурсам и методы обеспечения безопасности данных.
|
||||
91
labs/lab-08/other/Задание - Лабораторная работа 8.md
Normal file
91
labs/lab-08/other/Задание - Лабораторная работа 8.md
Normal 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
290
labs/lab-08/styles/menu.css
Normal 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;
|
||||
}
|
||||
}
|
||||
266
labs/lab-08/styles/order.css
Normal file
266
labs/lab-08/styles/order.css
Normal 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;
|
||||
}
|
||||
}
|
||||
190
labs/lab-08/styles/styles.css
Normal file
190
labs/lab-08/styles/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
42
labs/lab-08/templates/about.html
Normal file
42
labs/lab-08/templates/about.html
Normal 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>
|
||||
42
labs/lab-08/templates/delivery.html
Normal file
42
labs/lab-08/templates/delivery.html
Normal 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>
|
||||
198
labs/lab-08/templates/menu.html
Normal file
198
labs/lab-08/templates/menu.html
Normal 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>
|
||||
108
labs/lab-08/templates/order.html
Normal file
108
labs/lab-08/templates/order.html
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue