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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

103
labs/lab-09/index.html Normal file
View file

@ -0,0 +1,103 @@
<!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/orders.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-09/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-09/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 = '../templates/orders.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();
});

584
labs/lab-09/js/orders.js Normal file
View file

@ -0,0 +1,584 @@
const API_URL = 'https://edu.std-900.ist.mospolytech.ru/labs/api';
const API_KEY = '358a63a5-52ae-4ab0-800b-90f75ce5a5c2';
let orders = [];
let dishes = [];
let currentOrderId = null;
function loadOrders() {
console.log('Загрузка заказов...');
const url = API_URL + '/orders?api_key=' + API_KEY;
fetch(url)
.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);
orders = data;
sortOrders();
return loadDishes();
})
.then(function() {
displayOrders();
})
.catch(function(error) {
console.error('Ошибка при загрузке заказов:', error);
showNotification('Не удалось загрузить заказы: ' + error.message,
'error');
});
}
function loadDishes() {
console.log('Загрузка блюд...');
return fetch(API_URL + '/dishes')
.then(function(response) {
if (!response.ok) {
throw new Error('Ошибка загрузки блюд');
}
return response.json();
})
.then(function(data) {
console.log('Блюда загружены:', data.length);
dishes = data;
});
}
function sortOrders() {
orders.sort(function(a, b) {
const dateA = new Date(a.created_at);
const dateB = new Date(b.created_at);
return dateB - dateA;
});
}
function displayOrders() {
const ordersList = document.getElementById('orders-list');
const emptyOrders = document.getElementById('empty-orders');
if (!ordersList || !emptyOrders) {
console.error('Не найдены контейнеры для отображения заказов');
return;
}
if (orders.length === 0) {
ordersList.innerHTML = '';
emptyOrders.classList.remove('hidden');
return;
}
emptyOrders.classList.add('hidden');
ordersList.innerHTML = '';
orders.forEach(function(order, index) {
const orderCard = createOrderCard(order, index + 1);
ordersList.insertAdjacentHTML('beforeend', orderCard);
});
addOrderActionHandlers();
}
function createOrderCard(order, number) {
const date = formatDate(order.created_at);
const composition = getOrderComposition(order);
const price = calculateOrderPrice(order);
const deliveryTime = getDeliveryTime(order);
return `
<div class="order-card" data-order-id="${order.id}">
<div class="order-card-header">
<span class="order-number">Заказ ${number}</span>
<span class="order-date">${date}</span>
</div>
<div class="order-card-body">
<div class="order-info">
<p><strong>Состав:</strong> ${composition}</p>
<p><strong>Стоимость:</strong> ${price} руб.</p>
<p><strong>Доставка:</strong> ${deliveryTime}</p>
</div>
</div>
<div class="order-card-footer">
<button class="order-btn order-btn-view" data-action="view">Подробнее</button>
<button class="order-btn order-btn-edit" data-action="edit">Редактировать</button>
<button class="order-btn order-btn-delete" data-action="delete">Удалить</button>
</div>
</div>
`;
}
function formatDate(dateString) {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return day + '.' + month + '.' + year;
}
function formatDateTime(dateString) {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
}
function getOrderComposition(order) {
const dishNames = [];
if (order.soup_id) {
const dish = dishes.find(function(d) {
return d.id === order.soup_id;
});
if (dish) {
dishNames.push(dish.name);
}
}
if (order.main_course_id) {
const dish = dishes.find(function(d) {
return d.id === order.main_course_id;
});
if (dish) {
dishNames.push(dish.name);
}
}
if (order.salad_id) {
const dish = dishes.find(function(d) {
return d.id === order.salad_id;
});
if (dish) {
dishNames.push(dish.name);
}
}
if (order.drink_id) {
const dish = dishes.find(function(d) {
return d.id === order.drink_id;
});
if (dish) {
dishNames.push(dish.name);
}
}
if (order.dessert_id) {
const dish = dishes.find(function(d) {
return d.id === order.dessert_id;
});
if (dish) {
dishNames.push(dish.name);
}
}
return dishNames.join(', ');
}
function calculateOrderPrice(order) {
let total = 0;
if (order.soup_id) {
const dish = dishes.find(function(d) {
return d.id === order.soup_id;
});
if (dish) {
total += dish.price;
}
}
if (order.main_course_id) {
const dish = dishes.find(function(d) {
return d.id === order.main_course_id;
});
if (dish) {
total += dish.price;
}
}
if (order.salad_id) {
const dish = dishes.find(function(d) {
return d.id === order.salad_id;
});
if (dish) {
total += dish.price;
}
}
if (order.drink_id) {
const dish = dishes.find(function(d) {
return d.id === order.drink_id;
});
if (dish) {
total += dish.price;
}
}
if (order.dessert_id) {
const dish = dishes.find(function(d) {
return d.id === order.dessert_id;
});
if (dish) {
total += dish.price;
}
}
return total;
}
function getDeliveryTime(order) {
if (order.delivery_type === 'by_time') {
return order.delivery_time;
}
return 'Как можно скорее (с 7:00 до 23:00)';
}
function addOrderActionHandlers() {
const buttons = document.querySelectorAll('.order-btn');
buttons.forEach(function(button) {
button.addEventListener('click', function() {
const action = this.dataset.action;
const card = this.closest('.order-card');
const orderId = parseInt(card.dataset.orderId);
const order = orders.find(function(o) {
return o.id === orderId;
});
if (!order) {
console.error('Заказ не найден');
return;
}
if (action === 'view') {
showViewModal(order);
} else if (action === 'edit') {
showEditModal(order);
} else if (action === 'delete') {
showDeleteModal(order);
}
});
});
}
function showViewModal(order) {
const modal = document.getElementById('view-modal');
if (!modal) {
return;
}
const dateTime = formatDateTime(order.created_at);
const deliveryTime = getDeliveryTime(order);
document.getElementById('view-date').textContent = dateTime;
document.getElementById('view-name').textContent = order.full_name;
document.getElementById('view-address').textContent =
order.delivery_address;
document.getElementById('view-time').textContent = deliveryTime;
document.getElementById('view-phone').textContent = order.phone;
document.getElementById('view-email').textContent = order.email;
const commentSection = document.getElementById('view-comment-section');
const commentElement = document.getElementById('view-comment');
if (order.comment && order.comment.trim() !== '') {
commentSection.style.display = 'block';
commentElement.textContent = order.comment;
} else {
commentSection.style.display = 'none';
}
const itemsContainer = document.getElementById('view-items');
itemsContainer.innerHTML = '';
const orderItems = getOrderItems(order);
orderItems.forEach(function(item) {
const itemHTML = `
<div class="modal-order-item">
<span>${item.label}</span>
<span>${item.name} (${item.price}Р)</span>
</div>
`;
itemsContainer.insertAdjacentHTML('beforeend', itemHTML);
});
const total = calculateOrderPrice(order);
document.getElementById('view-total').textContent = total + 'Р';
modal.classList.remove('hidden');
}
function showEditModal(order) {
const modal = document.getElementById('edit-modal');
if (!modal) {
return;
}
currentOrderId = order.id;
const dateTime = formatDateTime(order.created_at);
document.getElementById('edit-created-date').textContent = dateTime;
document.getElementById('edit-name').value = order.full_name;
document.getElementById('edit-email').value = order.email;
document.getElementById('edit-phone').value = order.phone;
document.getElementById('edit-address').value = order.delivery_address;
document.getElementById('edit-time').value = order.delivery_time || '';
document.getElementById('edit-comment').value = order.comment || '';
const itemsContainer = document.getElementById('edit-items');
itemsContainer.innerHTML = '';
const orderItems = getOrderItems(order);
orderItems.forEach(function(item) {
const itemHTML = `
<div class="modal-order-item">
<span>${item.label}</span>
<span>${item.name} (${item.price}Р)</span>
</div>
`;
itemsContainer.insertAdjacentHTML('beforeend', itemHTML);
});
const total = calculateOrderPrice(order);
document.getElementById('edit-total').textContent = total + 'Р';
modal.classList.remove('hidden');
}
function showDeleteModal(order) {
const modal = document.getElementById('delete-modal');
if (!modal) {
return;
}
currentOrderId = order.id;
modal.classList.remove('hidden');
}
function getOrderItems(order) {
const items = [];
if (order.main_course_id) {
const dish = dishes.find(function(d) {
return d.id === order.main_course_id;
});
if (dish) {
items.push({
label: 'Основное блюдо',
name: dish.name,
price: dish.price
});
}
}
if (order.drink_id) {
const dish = dishes.find(function(d) {
return d.id === order.drink_id;
});
if (dish) {
items.push({
label: 'Напиток',
name: dish.name,
price: dish.price
});
}
}
if (order.dessert_id) {
const dish = dishes.find(function(d) {
return d.id === order.dessert_id;
});
if (dish) {
items.push({
label: 'Десерт',
name: dish.name,
price: dish.price
});
}
}
return items;
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('hidden');
}
currentOrderId = null;
}
function setupModalHandlers() {
const closeButtons = document.querySelectorAll('.modal-close');
closeButtons.forEach(function(button) {
button.addEventListener('click', function() {
const modal = this.closest('.modal');
if (modal) {
modal.classList.add('hidden');
}
currentOrderId = null;
});
});
const viewOkButton = document.getElementById('view-modal-ok');
if (viewOkButton) {
viewOkButton.addEventListener('click', function() {
closeModal('view-modal');
});
}
const editCancelButton = document.getElementById('edit-modal-cancel');
if (editCancelButton) {
editCancelButton.addEventListener('click', function() {
closeModal('edit-modal');
});
}
const editSaveButton = document.getElementById('edit-modal-save');
if (editSaveButton) {
editSaveButton.addEventListener('click', function() {
saveOrderEdit();
});
}
const deleteCancelButton = document.getElementById('delete-modal-cancel');
if (deleteCancelButton) {
deleteCancelButton.addEventListener('click', function() {
closeModal('delete-modal');
});
}
const deleteConfirmButton =
document.getElementById('delete-modal-confirm');
if (deleteConfirmButton) {
deleteConfirmButton.addEventListener('click', function() {
deleteOrder();
});
}
const modals = document.querySelectorAll('.modal');
modals.forEach(function(modal) {
modal.addEventListener('click', function(event) {
if (event.target === modal) {
modal.classList.add('hidden');
currentOrderId = null;
}
});
});
}
function saveOrderEdit() {
const form = document.getElementById('edit-form');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const updateData = {
full_name: formData.get('full_name'),
email: formData.get('email'),
phone: formData.get('phone'),
delivery_address: formData.get('delivery_address'),
delivery_type: formData.get('delivery_time') ? 'by_time' : 'now',
delivery_time: formData.get('delivery_time') || '',
comment: formData.get('comment') || ''
};
const url = API_URL + '/orders/' + currentOrderId + '?api_key=' + API_KEY;
console.log('Отправка изменений заказа:', updateData);
fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
})
.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);
closeModal('edit-modal');
showNotification('Заказ успешно изменён', 'success');
loadOrders();
})
.catch(function(error) {
console.error('Ошибка при редактировании заказа:', error);
showNotification('Не удалось изменить заказ: ' + error.message,
'error');
});
}
function deleteOrder() {
const url = API_URL + '/orders/' + currentOrderId + '?api_key=' + API_KEY;
console.log('Удаление заказа:', currentOrderId);
fetch(url, {
method: 'DELETE'
})
.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);
closeModal('delete-modal');
showNotification('Заказ успешно удалён', 'success');
loadOrders();
})
.catch(function(error) {
console.error('Ошибка при удалении заказа:', error);
showNotification('Не удалось удалить заказ: ' + error.message,
'error');
});
}
function showNotification(message, type) {
const notification = document.getElementById('notification');
if (!notification) {
return;
}
notification.textContent = message;
notification.className = 'notification notification-' + type;
notification.classList.remove('hidden');
setTimeout(function() {
notification.classList.add('hidden');
}, 3000);
}
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM загружен для страницы заказов');
setupModalHandlers();
loadOrders();
});

View file

@ -0,0 +1,16 @@
Модальные окна
1. Что такое модальное окно? В каких случаях оно используется?
2. Какие способы создания модальных окон вы знаете? Приведите примеры.
3. Каковы преимущества использования модального окна перед другими элементами интерфейса?
4. Назовите основные элементы структуры HTML-кода для реализации модального окна.
5. Как реализовать закрытие модального окна при нажатии на кнопку или за пределами самого окна?
6. Как сделать так, чтобы при открытии модального окна основной контент страницы затемнялся?
7. Какие стили CSS используются для позиционирования модального окна на странице?
8. Как предотвратить скроллинг основного контента страницы при открытом модальном окне?
Взаимодействие клиента и сервера
1. Опишите процесс обработки ошибок на сервере и отправку сообщений об ошибках клиенту. Какие коды состояния HTTP обычно используются для различных типов ошибок?
2. Что такое CORS (Cross-Origin Resource Sharing)? Для чего нужен этот механизм и как он реализуется на практике?
3. Почему важно учитывать производительность и оптимизацию запросов при взаимодействии клиента с сервером? Приведите примеры способов повышения эффективности взаимодействия.
4. Как можно организовать кэширование данных на стороне клиента и сервера? В чем преимущество использования кэширования и какие инструменты можно применять для этого?
5. Чем отличается статическое содержимое от динамически генерируемого содержимого на сервере? Приведите примеры задач, требующих генерации динамических страниц.

View file

@ -0,0 +1,75 @@
# Лабораторная работа № 9. Реализация страницы просмотра и управления оформленными заказами
---
Добавьте страницу просмотра истории заказов и реализуйте функции редактирования и удаления заказов.
## Порядок выполнения
1. Добавьте страницу "Заказы", разместите ссылку на неё в навигационном меню. Примерный макет страницы приведён ниже.
- На странице должен располагаться список заказов, оформленных ранее текущим пользователем. Сортировка списка по убыванию даты оформления (т. е. сначала новые заказы).
- Для каждого заказа должны отображаться: порядковый номер в списке, дата оформления заказа, состав заказа (названия блюд, перечисленные через запятую), стоимость заказа, время доставки (для заказов ко времени, для остальных подпись "Как можно скорее (с 7:00 до 23:00)"), кнопки "Подробнее", "Редактирование", "Удаление".
- Вместо кнопок можно использовать иконки (например, [Bootstrap Icons](https://icons.getbootstrap.com/)).
2. Реализуйте функциональность кнопок "Подробнее", "Редактирование", "Удаление".
- По нажатию на кнопку "Подробнее" должно открываться модальное окно с полной информацией о заказе (см. макет ниже).
- По нажатию на кнопку "Редактирование" должно открываться модальное окно с формой редактирования заказа (см. макет ниже). Значения полей формы должны быть установлены значениями соответствующих полей редактируемого заказа. Для редактирования доступны поля full_name, email, phone, delivery_address, delivery_type, delivery_time, comment.
- По нажатию на кнопку "Удаление" должно открываться модальное окно подтверждения удаления (см. макет ниже).
- В правом верхнем углу каждого модального окна должен быть "крестик", по нажатию на который модальное окно закрывается. Внизу модального окна должны располагаться кнопки действий: для просмотра "Ок" (закрытие окна), для редактирования "Сохранить" (отправка данных на сервер) и "Отмена" (закрытие окна), для удаления "Да" (отправка запроса на сервер) и "Отмена" (закрытие окна).
- При возникновении ошибок при отправке запроса или его обработке на стороне сервера пользователю должно быть отображено уведомление об ошибке. В случае успешного выполнения операции (редактирования, удаления), модальное окно должно быть скрыто, и пользователю должно быть отображено уведомление об успешном завершении операции (например, "Заказ успешно изменён"). Формат отображения уведомлений на усмотрение студента.
- При успешном изменении или удалении заказа список заказов должен быть обновлён (т. е. удалённый заказ должен быть убран из списка, а информация об изменившемся заказе должна быть обновлена).
## Инструкция по работе с 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} нужно подставить целое число идентификатор заказа.|
### Материалы для изучения
[Что такое модальные окна [SkillBox]](https://skillbox.ru/media/code/chto-takoe-modalnye-okna-i-kak-ikh-effektivno-ispolzovat/)
[Попап [Doka]](https://doka.guide/recipes/popup/)
[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)

298
labs/lab-09/styles/menu.css Normal file
View file

@ -0,0 +1,298 @@
.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;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.section-header h2 {
margin: 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;
}
nav {
justify-content: space-between;
}
.about-company img {
width: 500px;
}
.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,414 @@
.orders-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.empty-orders-message {
text-align: center;
padding: 60px 20px;
}
.empty-orders-message.hidden {
display: none;
}
.empty-orders-message p {
font-size: 18px;
color: #666;
line-height: 1.6;
}
.empty-orders-message a {
color: #2d5016;
text-decoration: underline;
font-weight: 600;
}
.order-card {
background-color: white;
border-radius: 15px;
padding: 25px 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s;
}
.order-card:hover {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.order-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.order-number {
font-size: 20px;
font-weight: 700;
color: #2d5016;
}
.order-date {
font-size: 16px;
color: #666;
}
.order-card-body {
margin-bottom: 20px;
}
.order-info p {
margin: 10px 0;
font-size: 16px;
line-height: 1.6;
}
.order-info strong {
color: #2d5016;
}
.order-card-footer {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.order-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.order-btn-view {
background-color: #2d5016;
color: white;
}
.order-btn-view:hover {
background-color: #3d6020;
}
.order-btn-edit {
background-color: #f1eee9;
color: #333;
}
.order-btn-edit:hover {
background-color: #e0ddd8;
}
.order-btn-delete {
background-color: tomato;
color: white;
}
.order-btn-delete:hover {
background-color: #ff4500;
}
.modal {
display: flex;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal.hidden {
display: none;
}
.modal-content {
background-color: white;
border-radius: 15px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
}
.modal-content-small {
max-width: 400px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h3 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #000;
}
.modal-close {
font-size: 28px;
font-weight: 400;
color: #999;
cursor: pointer;
line-height: 1;
transition: color 0.3s;
}
.modal-close:hover {
color: #333;
}
.modal-body {
padding: 20px 30px;
}
.modal-section {
margin-bottom: 25px;
}
.modal-section:last-child {
margin-bottom: 0;
}
.modal-section-title {
font-size: 16px;
font-weight: 700;
color: #000;
margin: 0 0 15px 0;
}
.modal-label {
font-size: 14px;
color: #666;
display: block;
margin-bottom: 5px;
}
.modal-value {
font-size: 16px;
color: #000;
display: block;
}
.modal-info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.modal-info-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.modal-order-items {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.modal-order-item {
display: flex;
justify-content: space-between;
font-size: 16px;
color: #000;
}
.modal-order-item span:first-child {
color: #666;
}
.modal-total {
display: flex;
justify-content: space-between;
font-size: 16px;
font-weight: 700;
color: #000;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
.modal-delete-text {
font-size: 16px;
color: #000;
text-align: center;
margin: 0;
}
.modal-body form label {
display: block;
font-size: 14px;
color: #000;
margin: 0 0 8px 0;
font-weight: 400;
}
.modal-body form input[type="text"],
.modal-body form input[type="email"],
.modal-body form input[type="tel"],
.modal-body form input[type="time"],
.modal-body form textarea {
width: 100%;
padding: 10px 12px;
margin: 0 0 15px 0;
border: 1px solid #ddd;
border-radius: 5px;
font-family: 'Montserrat', sans-serif;
font-size: 15px;
}
.modal-body form textarea {
resize: vertical;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 20px 30px;
border-top: 1px solid #e0e0e0;
}
.modal-btn {
padding: 10px 30px;
border: none;
border-radius: 5px;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
min-width: 100px;
}
.modal-btn-ok {
background-color: #e8e8e8;
color: #000;
}
.modal-btn-ok:hover {
background-color: #d0d0d0;
}
.modal-btn-cancel {
background-color: #e8e8e8;
color: #000;
}
.modal-btn-cancel:hover {
background-color: #d0d0d0;
}
.modal-btn-save {
background-color: #4caf50;
color: white;
}
.modal-btn-save:hover {
background-color: #45a049;
}
.modal-btn-delete {
background-color: #c62828;
color: white;
}
.modal-btn-delete:hover {
background-color: #b71c1c;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 10px;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
font-weight: 600;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
z-index: 2000;
animation: slideIn 0.3s ease-out;
}
.notification.hidden {
display: none;
}
.notification-success {
background-color: #4caf50;
color: white;
}
.notification-error {
background-color: #c62828;
color: white;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 800px) {
.modal-content {
width: 95%;
}
.order-card-footer {
flex-direction: column;
}
.order-btn {
width: 100%;
}
}
@media (max-width: 600px) {
.modal-header {
padding: 15px 20px;
}
.modal-body {
padding: 15px 20px;
}
.modal-footer {
padding: 15px 20px;
flex-direction: column;
}
.modal-btn {
width: 100%;
}
.notification {
right: 10px;
left: 10px;
text-align: center;
}
}

View file

@ -0,0 +1,180 @@
* {
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;
}
h2 {
font-size: 28px;
color: #2d5016;
margin: 0 0 20px 0;
}
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;
}
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: 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,43 @@
<!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="orders.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,43 @@
<!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="orders.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,199 @@
<!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="orders.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,109 @@
<!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="orders.html">Мои заказы</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>

View file

@ -0,0 +1,178 @@
<!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/orders.css">
</head>
<body>
<header>
<h1>ЭкоЛанч</h1>
<nav>
<a href="../index.html">Главная</a>
<a href="menu.html">Собрать ланч</a>
<a href="order.html">Оформить заказ</a>
<a href="orders.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="orders-list" class="orders-list"></div>
<div id="empty-orders" class="empty-orders-message hidden">
<p>У вас пока нет оформленных заказов. Перейдите на страницу <a href="menu.html">Собрать ланч</a>, чтобы создать первый заказ.</p>
</div>
</section>
</main>
<div id="view-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Просмотр заказа</h3>
<span class="modal-close">&times;</span>
</div>
<div class="modal-body" id="view-modal-body">
<div class="modal-section">
<p class="modal-label">Дата оформления</p>
<p class="modal-value" id="view-date"></p>
</div>
<div class="modal-section">
<h4 class="modal-section-title">Доставка</h4>
<div class="modal-info-grid">
<div class="modal-info-item">
<span class="modal-label">Имя получателя</span>
<span class="modal-value" id="view-name"></span>
</div>
<div class="modal-info-item">
<span class="modal-label">Адрес доставки</span>
<span class="modal-value" id="view-address"></span>
</div>
<div class="modal-info-item">
<span class="modal-label">Время доставки</span>
<span class="modal-value" id="view-time"></span>
</div>
<div class="modal-info-item">
<span class="modal-label">Телефон</span>
<span class="modal-value" id="view-phone"></span>
</div>
<div class="modal-info-item">
<span class="modal-label">Email</span>
<span class="modal-value" id="view-email"></span>
</div>
</div>
</div>
<div class="modal-section" id="view-comment-section">
<h4 class="modal-section-title">Комментарий</h4>
<p class="modal-value" id="view-comment"></p>
</div>
<div class="modal-section">
<h4 class="modal-section-title">Состав заказа</h4>
<div class="modal-order-items" id="view-items"></div>
<div class="modal-total">
<span>Стоимость:</span>
<span id="view-total"></span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-ok" id="view-modal-ok">OK</button>
</div>
</div>
</div>
<div id="edit-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Редактирование заказа</h3>
<span class="modal-close">&times;</span>
</div>
<div class="modal-body">
<div class="modal-section">
<p class="modal-label">Дата оформления</p>
<p class="modal-value" id="edit-created-date"></p>
</div>
<form id="edit-form">
<div class="modal-section">
<h4 class="modal-section-title">Доставка</h4>
<label for="edit-name">Имя получателя</label>
<input type="text" id="edit-name" name="full_name" required>
<label for="edit-address">Адрес доставки</label>
<input type="text" id="edit-address" name="delivery_address" required>
<label for="edit-time">Время доставки</label>
<input type="time" id="edit-time" name="delivery_time" min="07:00" max="23:00" step="300">
<label for="edit-phone">Телефон</label>
<input type="tel" id="edit-phone" name="phone" required>
<label for="edit-email">Email</label>
<input type="email" id="edit-email" name="email" required>
</div>
<div class="modal-section">
<h4 class="modal-section-title">Комментарий</h4>
<textarea id="edit-comment" name="comment" rows="3"></textarea>
</div>
<div class="modal-section">
<h4 class="modal-section-title">Состав заказа</h4>
<div class="modal-order-items" id="edit-items"></div>
<div class="modal-total">
<span>Стоимость:</span>
<span id="edit-total"></span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-cancel" id="edit-modal-cancel">Отмена</button>
<button class="modal-btn modal-btn-save" id="edit-modal-save">Сохранить</button>
</div>
</div>
</div>
<div id="delete-modal" class="modal hidden">
<div class="modal-content modal-content-small">
<div class="modal-header">
<h3>Удаление заказа</h3>
<span class="modal-close">&times;</span>
</div>
<div class="modal-body">
<p class="modal-delete-text">Вы уверены, что хотите удалить заказ?</p>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-cancel" id="delete-modal-cancel">Отмена</button>
<button class="modal-btn modal-btn-delete" id="delete-modal-confirm">Да</button>
</div>
</div>
</div>
<div id="notification" class="notification hidden"></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/orders.js"></script>
</body>
</html>