diff --git a/labs/lab-09/img/main.jpg b/labs/lab-09/img/main.jpg new file mode 100644 index 0000000..f9f6558 Binary files /dev/null and b/labs/lab-09/img/main.jpg differ diff --git a/labs/lab-09/index.html b/labs/lab-09/index.html new file mode 100644 index 0000000..5e173cb --- /dev/null +++ b/labs/lab-09/index.html @@ -0,0 +1,103 @@ + + + + + + + ЭкоЛанч - Доставка здоровых бизнес-ланчей в Москве + + + + + + +
+

ЭкоЛанч

+ +
+ +
+
+

О компании ЭкоЛанч

+ Здоровая еда в контейнерах +

ЭкоЛанч - это современная компания по доставке здоровых бизнес-ланчей в офисы Москвы. Наша миссия - сделать правильное питание доступным для занятых людей, которые ценят свое время и здоровье.

+

Мы используем только свежие продукты от проверенных поставщиков, а наши повара готовят блюда каждое утро. Все ланчи упакованы в экологичную биоразлагаемую упаковку, потому что мы заботимся не только о вашем здоровье, но и о здоровье нашей планеты.

+
+ +
+

Наши преимущества

+ +
+ + +
+ + + + \ No newline at end of file diff --git a/labs/lab-09/js/menu.js b/labs/lab-09/js/menu.js new file mode 100644 index 0000000..e0611a4 --- /dev/null +++ b/labs/lab-09/js/menu.js @@ -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 ` +
+ ${dish.name} +

${dish.price} руб.

+

${dish.name}

+

${dish.count}

+ +
+ `; +} + +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(); +}); \ No newline at end of file diff --git a/labs/lab-09/js/order.js b/labs/lab-09/js/order.js new file mode 100644 index 0000000..b1ac0c4 --- /dev/null +++ b/labs/lab-09/js/order.js @@ -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 ` +
+ ${dish.name} +

${dish.price} руб.

+

${dish.name}

+

${dish.count}

+ +
+ `; +} + +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 = + '

Ничего не выбрано

'; + return; + } + + let summaryHTML = ''; + let totalPrice = 0; + + if (orderDishes.soup) { + summaryHTML += ` +
+

Суп

+
+ ${orderDishes.soup.name} + ${orderDishes.soup.price} руб. +
+
+ `; + totalPrice += orderDishes.soup.price; + } else { + summaryHTML += ` +
+

Суп

+

Блюдо не выбрано

+
+ `; + } + + if (orderDishes['main-course']) { + summaryHTML += ` +
+

Главное блюдо

+
+ ${orderDishes['main-course'].name} + ${orderDishes['main-course'].price} руб. +
+
+ `; + totalPrice += orderDishes['main-course'].price; + } else { + summaryHTML += ` +
+

Главное блюдо

+

Блюдо не выбрано

+
+ `; + } + + if (orderDishes.salad) { + summaryHTML += ` +
+

Салат

+
+ ${orderDishes.salad.name} + ${orderDishes.salad.price} руб. +
+
+ `; + totalPrice += orderDishes.salad.price; + } else { + summaryHTML += ` +
+

Салат

+

Блюдо не выбрано

+
+ `; + } + + if (orderDishes.drink) { + summaryHTML += ` +
+

Напиток

+
+ ${orderDishes.drink.name} + ${orderDishes.drink.price} руб. +
+
+ `; + totalPrice += orderDishes.drink.price; + } else { + summaryHTML += ` +
+

Напиток

+

Напиток не выбран

+
+ `; + } + + if (orderDishes.dessert) { + summaryHTML += ` +
+

Десерт

+
+ ${orderDishes.dessert.name} + ${orderDishes.dessert.price} руб. +
+
+ `; + totalPrice += orderDishes.dessert.price; + } else { + summaryHTML += ` +
+

Десерт

+

Десерт не выбран

+
+ `; + } + + summaryHTML += ` +
+

Стоимость заказа

+

${totalPrice} руб.

+
+ `; + + 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(); +}); \ No newline at end of file diff --git a/labs/lab-09/js/orders.js b/labs/lab-09/js/orders.js new file mode 100644 index 0000000..242e40e --- /dev/null +++ b/labs/lab-09/js/orders.js @@ -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 ` +
+
+ Заказ №${number} + ${date} +
+
+
+

Состав: ${composition}

+

Стоимость: ${price} руб.

+

Доставка: ${deliveryTime}

+
+
+ +
+ `; +} + +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 = ` + + `; + 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 = ` + + `; + 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(); +}); \ No newline at end of file diff --git a/labs/lab-09/other/Вопросы для подготовки к защите ЛР №9.txt b/labs/lab-09/other/Вопросы для подготовки к защите ЛР №9.txt new file mode 100644 index 0000000..48399f8 --- /dev/null +++ b/labs/lab-09/other/Вопросы для подготовки к защите ЛР №9.txt @@ -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. Чем отличается статическое содержимое от динамически генерируемого содержимого на сервере? Приведите примеры задач, требующих генерации динамических страниц. \ No newline at end of file diff --git a/labs/lab-09/other/Задание - Лабораторная работа 9.md b/labs/lab-09/other/Задание - Лабораторная работа 9.md new file mode 100644 index 0000000..1d31797 --- /dev/null +++ b/labs/lab-09/other/Задание - Лабораторная работа 9.md @@ -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.
Доступное время доставки: с 7:00 до 23:00 с шагом 5 минут.
Не должно быть пустым, если delivery_type=by_time.
Не должно быть раньше текущего времени.| +|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} нужно подставить целое число – идентификатор заказа.
Достаточно передать только значения изменившихся полей.| +|Удалить заказ|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) \ No newline at end of file diff --git a/labs/lab-09/styles/menu.css b/labs/lab-09/styles/menu.css new file mode 100644 index 0000000..8e05438 --- /dev/null +++ b/labs/lab-09/styles/menu.css @@ -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; + } +} \ No newline at end of file diff --git a/labs/lab-09/styles/order.css b/labs/lab-09/styles/order.css new file mode 100644 index 0000000..aed29b7 --- /dev/null +++ b/labs/lab-09/styles/order.css @@ -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; + } +} \ No newline at end of file diff --git a/labs/lab-09/styles/orders.css b/labs/lab-09/styles/orders.css new file mode 100644 index 0000000..6e88dbc --- /dev/null +++ b/labs/lab-09/styles/orders.css @@ -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; + } +} \ No newline at end of file diff --git a/labs/lab-09/styles/styles.css b/labs/lab-09/styles/styles.css new file mode 100644 index 0000000..158c58c --- /dev/null +++ b/labs/lab-09/styles/styles.css @@ -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; + } +} \ No newline at end of file diff --git a/labs/lab-09/templates/about.html b/labs/lab-09/templates/about.html new file mode 100644 index 0000000..f09e109 --- /dev/null +++ b/labs/lab-09/templates/about.html @@ -0,0 +1,43 @@ + + + + + + + О нас - ЭкоЛанч + + + + + + +
+

ЭкоЛанч

+ +
+ +
+
+

О нашей компании

+

ЭкоЛанч - это команда профессионалов, которые заботятся о вашем здоровье и комфорте. Мы готовим вкусные и полезные блюда из свежих продуктов и доставляем их прямо в ваш офис.

+

Наша миссия - сделать здоровое питание доступным и удобным для каждого занятого человека в Москве.

+
+
+ + + + \ No newline at end of file diff --git a/labs/lab-09/templates/delivery.html b/labs/lab-09/templates/delivery.html new file mode 100644 index 0000000..2b510f8 --- /dev/null +++ b/labs/lab-09/templates/delivery.html @@ -0,0 +1,43 @@ + + + + + + + Доставка - ЭкоЛанч + + + + + + +
+

ЭкоЛанч

+ +
+ +
+
+

Условия доставки

+

Мы осуществляем доставку здоровых бизнес-ланчей по всей Москве. Доставка производится с понедельника по пятницу с 7:00 до 23:00.

+

Доставка по всей Москве в пределах МКАД осуществляется бесплатно!

+
+
+ + + + \ No newline at end of file diff --git a/labs/lab-09/templates/menu.html b/labs/lab-09/templates/menu.html new file mode 100644 index 0000000..ecaf51a --- /dev/null +++ b/labs/lab-09/templates/menu.html @@ -0,0 +1,199 @@ + + + + + + + Собрать ланч - ЭкоЛанч + + + + + + + +
+

ЭкоЛанч

+ +
+
+
+

Доступные для заказа варианты ланча

+
+
+
+ 🍲 +

Суп

+
+
+ 🍽️ +

Главное блюдо

+
+
+ 🥗 +

Салат

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍲 +

Суп

+
+
+ 🍽️ +

Главное блюдо

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍲 +

Суп

+
+
+ 🥗 +

Салат

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍽️ +

Главное блюдо

+
+
+ 🥗 +

Салат

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍽️ +

Главное блюдо

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍰 +

Десерт

+
+

(Можно добавить к любому заказу)

+
+
+

Десерты можно добавить к любому варианту ланча

+
+ +
+
+

Супы

+
+ + + +
+
+
+
+ +
+
+

Главные блюда

+
+ + + +
+
+
+
+ +
+
+

Салаты и стартеры

+
+ + + +
+
+
+
+ +
+
+

Напитки

+
+ + +
+
+
+
+ +
+
+

Десерты

+
+ + + +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/labs/lab-09/templates/order.html b/labs/lab-09/templates/order.html new file mode 100644 index 0000000..b6e25de --- /dev/null +++ b/labs/lab-09/templates/order.html @@ -0,0 +1,109 @@ + + + + + + + Оформить заказ - ЭкоЛанч + + + + + + + + +
+

ЭкоЛанч

+ +
+ +
+
+

Состав заказа

+
+ +
+ +
+

Оформление заказа

+
+
+
+

Ваш заказ

+
+

Ничего не выбрано

+
+
+ +
+

Данные для доставки

+ + + + + + + +
+ + +
+ + + + + + + Доставка осуществляется только по Москве + +
+ +
+ + +
+
+ + +
+
+ + + + Доступное время доставки с 7:00 до 23:00 + + + + +
+ + +
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/labs/lab-09/templates/orders.html b/labs/lab-09/templates/orders.html new file mode 100644 index 0000000..1fb7ea2 --- /dev/null +++ b/labs/lab-09/templates/orders.html @@ -0,0 +1,178 @@ + + + + + + + Мои заказы - ЭкоЛанч + + + + + + + +
+

ЭкоЛанч

+ +
+ +
+
+

Мои заказы

+
+ +
+
+ + + + + + + + + + + + + + \ No newline at end of file