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

ЭкоЛанч

+ +
+ +
+
+

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

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

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

+

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

+
+ +
+

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

+ +
+ + +
+ + + + \ No newline at end of file diff --git a/labs/lab-08/js/menu.js b/labs/lab-08/js/menu.js new file mode 100644 index 0000000..e0611a4 --- /dev/null +++ b/labs/lab-08/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-08/js/order.js b/labs/lab-08/js/order.js new file mode 100644 index 0000000..4784131 --- /dev/null +++ b/labs/lab-08/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 = '../index.html'; + }) + .catch(function(error) { + console.error('Ошибка при отправке заказа:', error); + alert('Не удалось оформить заказ: ' + error.message); + }); +} + +function setupResetButton() { + const resetButton = document.getElementById('reset-button'); + + if (!resetButton) { + console.error('Кнопка сброса не найдена'); + return; + } + + resetButton.addEventListener('click', function() { + const form = document.getElementById('order-form'); + form.reset(); + }); +} + +document.addEventListener('DOMContentLoaded', function() { + console.log('DOM загружен для страницы заказа'); + loadDishes(); + setupFormSubmission(); + setupResetButton(); +}); \ No newline at end of file diff --git a/labs/lab-08/other/Вопросы для подготовки к защите ЛР №8.txt b/labs/lab-08/other/Вопросы для подготовки к защите ЛР №8.txt new file mode 100644 index 0000000..2df061a --- /dev/null +++ b/labs/lab-08/other/Вопросы для подготовки к защите ЛР №8.txt @@ -0,0 +1,19 @@ +Local storage +1. Что такое localStorage? +2. Какие ограничения имеет localStorage по объему хранимой информации? +3. Как добавить данные в localStorage с помощью JavaScript? +4. Можно ли хранить объекты в localStorage напрямую? Если нет, то как это сделать? +5. Какой метод используется для удаления конкретного ключа из localStorage? +6. Чем отличается localStorage от sessionStorage? +7. Может ли localStorage быть использован для хранения конфиденциальных данных? Почему да/почему нет? +8. Назовите преимущества и недостатки использования localStorage. +9. Есть ли возможность очистки всего содержимого localStorage одним вызовом метода? +10. Когда данные в localStorage могут быть автоматически удалены? + + +Взаимодействие клиента и сервера +1. Что такое клиент-серверная архитектура? В чем заключается ее суть и какие компоненты она включает? +2. Какие протоколы передачи данных используются в веб-разработке для взаимодействия клиента с сервером? Перечислите основные протоколы и объясните их назначение. +3. Что такое RESTful API? Дайте определение и приведите пример реализации простого RESTful сервиса. +4. Объясните принцип работы сессий и куков. Как они помогают сохранять состояние пользователя на стороне сервера и клиента соответственно? +5. Как осуществляется аутентификация и авторизация на уровне сервера? Описать подходы к защите доступа к ресурсам и методы обеспечения безопасности данных. \ No newline at end of file diff --git a/labs/lab-08/other/Задание - Лабораторная работа 8.md b/labs/lab-08/other/Задание - Лабораторная работа 8.md new file mode 100644 index 0000000..516d5fe --- /dev/null +++ b/labs/lab-08/other/Задание - Лабораторная работа 8.md @@ -0,0 +1,91 @@ +# Лабораторная работа № 8. Реализация функциональности оформления заказа + +--- + +Реализуйте сохранение выбранных пользователем блюд, чтобы после обновления страницы выбор пользователя не сбрасывался. Добавьте страницу оформления заказа, где будет отображаться текущий состав заказа и форма ввода данных. Реализуйте возможность оформления заказа – отправку данных на сервер. + +## Порядок выполнения + +1. Реализуйте сохранение данных о выбранных пользователем блюдах в localStorage, чтобы в последующем иметь возможность получить эти данные на странице "Оформить заказ". В localStorage должны храниться только идентификаторы выбранных блюд. Формат хранения данных выбирается на усмотрение студента. + +2. Добавьте страницу "Оформить заказ". Внешний вид страницы представлен на макете ниже. Добавьте ссылку на эту страницу в навигационное меню. На странице "Оформить заказ" должно быть два раздела: "Состав заказа" и "Оформление заказа". + - В разделе "Состав заказа" должны быть отображены ранее добавленные пользователем в заказ блюда (список выбранных блюд нужно загрузить из localStorage, а данные для отображения – загрузить с сервера). + - Формат отображения блюд такой же, как на странице "Собрать ланч". + - Вместо кнопки "Добавить" в карточке блюда должна быть кнопка "Удалить", по нажатию на которую соответствующее блюдо удаляется из заказа (т. е. информация о нём удаляется из localStorage) и карточка удаляется со страницы. + - Если ни одно блюдо не выбрано, нужно отобразить пользователю текст "Ничего не выбрано. Чтобы добавить блюда в заказ, перейдите на страницу Собрать ланч.", где текст "Собрать ланч" – гиперссылка на соответствующую страницу. + - В разделе "Оформление заказа" должна отображаться форма оформления заказа, перенесённая со страницы "Собрать ланч". + - Выбранные пользователем блюда отображаются в левой части формы (см. макет выше), напротив каждого блюда отображается его стоимость. + - При удалении блюда из заказа в соответствующем ему пункте должен отобразиться текст "Не выбран" (или "Не выбрано" для "Главного блюда") и должна быть пересчитана итоговая стоимость заказа. + - Перед отправкой данных формы на сервер должна производиться проверка соответствия состава заказа одному из доступных комбо (см. задание к ЛР 6). + +3. Внесите исправления в страницу "Собрать ланч": вместо формы оформления заказа разместите панель для перехода к оформлению заказа, в которой будет отображаться текущая стоимость добавленных в заказ блюд и ссылка "Перейти к оформлению", ведущая на страницу оформления заказа. + - Панель для перехода к оформлению заказа должна быть скрыта, если пользователь не добавил в заказ ни одного блюда. + - Ссылка "Перейти к оформлению" должна быть недоступна до тех пор, пока сосав заказа не удовлетворяет одному из доступных комбо (см. задание к ЛР 6). + - Для размещения элемента на странице используйте position: sticky. + - При добавлении очередного блюда в заказ, стоимость должна пересчитываться. + - При открытии страницы "Собрать ланч" должны учитываться данные текущего заказа, хранящиеся в localStorage (т. е. в меню должны быть выделены выбранные блюда, рассчитана и отображена текущая суммарная стоимость выбранных блюд и т. д.). + +4. Реализуйте отправку данных заказа на сервер по нажатию на кнопку "Отправить". + - Запрос должен быть отправлен при помощи fetch. + - В случае возникновения ошибки при отправке запроса или его обработке на стороне сервера пользователю должно быть отображено уведомление об ошибке (можно использовать функцию alert или написать кастомное модальное окно – на ваше усмотрение). + - После успешного оформления заказа данные о выбранных блюдах из localStorage должны удаляться. Если не удалось оформить заказ – данные не удаляются. + +## Инструкция по работе с API + +Для получения доступа к API необходимо пройти процедуру авторизации. Для авторизации нужно указать в качестве параметра запроса api_key значение уникального ключа, который выдаётся каждому пользователю. Ключ представляет собой идентификатор UUIDv4, который является случайным 16-байтным номером (например, 123e4567-e89b-12d3-a456-426655440000). + +**Обратите внимание, что параметр api_key всегда передаётся в строке запроса.** + +Пользователь может просматривать, редактировать и удалять только свои заказы. В один момент времени в базе данных может быть не более 10 заказов, созданных одним и тем же пользователем. + +Если пользователь попробует совершить действие, не пройдя авторизацию, в качестве ответа на его запрос придёт сообщение: + +{"error": "Для получения доступа к API необходимо пройти процедуру авторизации. Для этого нужно передать в запросе персональный API Key."} + +При передаче параметров в POST- и PUT-запросах данные должны передаваться в теле запроса в формате application/json (нужно вручную сериализовать данные и установить значение заголовка Content-Type) или multipart/form-data (достаточно при отправке данных формы воспользоваться объектом FormData). + +При оформлении заказа в запросе к серверу должны быть указаны значения обязательных полей: + +|Название|Тип|Обязательное|Только для чтения|Примечание| +|---|---|---|---|---| +|id|Integer||Да|Устанавливается сервером.| +|full_name|String|Да||| +|email|String|Да|| +|subscribe|Boolean|Нет||Допустимы значения 0 и 1.| +|phone|String|Да||| +|delivery_address|String|Да||| +|delivery_type|String|Да||Допустимы значения "now" и "by_time".| +|delivery_time|Time|Нет||Значение передаётся в формате HH:MM.
Доступное время доставки: с 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} нужно подставить целое число – идентификатор заказа.| + +### Материалы для изучения +[localStorage [Doka]](https://doka.guide/js/local-storage/) +[Получение данных с сервера [MDN]](https://developer.mozilla.org/ru/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data) +[Работа с JSON [MDN]](https://developer.mozilla.org/ru/docs/Learn/JavaScript/Objects/JSON) +[Промисы, async/await [Learn JS]](https://learn.javascript.ru/async) +[async/await [Doka]](https://doka.guide/js/async-await/) +[Использование промисов [MDN]](https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Using_promises) +[Fetch [Learn JS]](https://learn.javascript.ru/fetch) +[Fetch API [MDN]](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) +[fetch() [Doka]](https://doka.guide/js/fetch/) +[Cross-Origin Resource Sharing (CORS) [MDN]](https://developer.mozilla.org/ru/docs/Web/HTTP/CORS) \ No newline at end of file diff --git a/labs/lab-08/styles/menu.css b/labs/lab-08/styles/menu.css new file mode 100644 index 0000000..6da4b73 --- /dev/null +++ b/labs/lab-08/styles/menu.css @@ -0,0 +1,290 @@ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.section-header h2 { + margin: 0; +} + +.combo-section { + margin-bottom: 50px; +} + +.combo-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 20px; + margin-bottom: 20px; +} + +.combo-card { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 20px; + background-color: white; + padding: 20px; + border-radius: 15px; +} + +.combo-dessert { + flex-direction: column; + gap: 10px; +} + +.combo-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.combo-icon { + font-size: 50px; + display: block; + margin: 0 0 8px 0; + transition: transform 0.3s; +} + +.combo-item:hover .combo-icon { + transform: translateY(-5px) scale(1.1); +} + +.combo-item p { + margin: 0; + font-size: 14px; + text-align: center; +} + +.combo-note-inline { + font-size: 12px; + text-align: center; + color: #666; + margin: 0; + font-style: italic; +} + +.combo-note { + font-size: 14px; + color: #666; + font-style: italic; + text-align: center; + margin: 10px 0 0 0; +} + +.filter-buttons { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.filter-btn { + background-color: white; + border: 2px solid #2d5016; + color: #2d5016; + padding: 10px 20px; + border-radius: 10px; + cursor: pointer; + font-family: 'Montserrat', sans-serif; + font-size: 16px; + font-weight: 600; + transition: all 0.3s; +} + +.filter-btn:hover { + background-color: #f1eee9; +} + +.filter-btn.active { + background-color: #2d5016; + color: white; +} + +.dishes-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; +} + +.dish-card { + display: flex; + flex-direction: column; + padding: 30px 40px; + border-radius: 35px; + cursor: pointer; + filter: drop-shadow(17px 19px 24px rgba(0, 0, 0, 0.13)); + background-color: white; +} + +.dish-card.selected { + border: 3px solid #2d5016; + background-color: #f0f7ec; +} + +.dish-card.selected button { + background-color: #2d5016; + color: white; +} + +.dish-card:hover { + border: 2px solid tomato; +} + +.dish-card:hover button { + background-color: tomato; + color: white; +} + +.dish-card img { + width: 100%; + height: auto; + aspect-ratio: 12 / 9; + object-fit: cover; + border-radius: 35px; + margin: 0 0 15px 0; +} + +.dish-price { + font-size: 20px; + font-weight: 600; + margin: 0 0 10px 0; +} + +.dish-name { + font-size: 18px; + font-weight: 600; + margin: 0 0 10px 0; +} + +.dish-weight { + color: #888; + margin: 0 0 10px 0; + margin-top: auto; +} + +.dish-card button { + background-color: #f1eee9; + border: none; + padding: 10px 30px; + border-radius: 10px; + cursor: pointer; + font-family: 'Montserrat', sans-serif; + font-size: 16px; + margin-top: 10px; +} + +.remove-dish-btn { + background-color: tomato; + color: white; +} + +.remove-dish-btn:hover { + background-color: #ff4500; +} + +.order-panel { + position: sticky; + bottom: 20px; + background-color: white; + padding: 20px 40px; + margin: 0 auto; + max-width: 1200px; + border-radius: 15px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2); + z-index: 100; +} + +.order-panel.hidden { + display: none; +} + +.order-panel-content { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; +} + +.order-panel-info { + display: flex; + align-items: center; + gap: 15px; +} + +.order-panel-label { + font-size: 18px; + font-weight: 600; + margin: 0; +} + +.order-panel-price { + font-size: 24px; + font-weight: 700; + color: #2d5016; + margin: 0; +} + +.order-panel-button { + background-color: #2d5016; + color: white; + text-decoration: none; + padding: 15px 40px; + border-radius: 10px; + font-family: 'Montserrat', sans-serif; + font-size: 16px; + font-weight: 600; + transition: all 0.3s; +} + +.order-panel-button:hover { + background-color: #3d6020; +} + +.order-panel-button.disabled { + background-color: #ccc; + color: #888; + cursor: not-allowed; + pointer-events: none; +} + +@media (max-width: 800px) { + .combo-grid { + grid-template-columns: 1fr 1fr; + } + + .dishes-grid { + grid-template-columns: 1fr 1fr; + } + + .order-panel-content { + flex-direction: column; + } +} + +@media (max-width: 600px) { + .combo-grid { + grid-template-columns: 1fr; + } + + .dishes-grid { + grid-template-columns: 1fr; + } + + .order-panel { + padding: 15px 20px; + } + + .order-panel-content { + flex-direction: column; + align-items: stretch; + } + + .order-panel-button { + text-align: center; + } +} \ No newline at end of file diff --git a/labs/lab-08/styles/order.css b/labs/lab-08/styles/order.css new file mode 100644 index 0000000..aed29b7 --- /dev/null +++ b/labs/lab-08/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-08/styles/styles.css b/labs/lab-08/styles/styles.css new file mode 100644 index 0000000..58e6c37 --- /dev/null +++ b/labs/lab-08/styles/styles.css @@ -0,0 +1,190 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: 'Montserrat', sans-serif; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +header { + background-color: #2d5016; + color: white; + padding: 20px 40px; + margin: 0; +} + +h1 { + margin: 0 0 15px 0; + font-size: 36px; + color: white; +} + +nav { + margin: 0; + padding: 10px 0; + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +nav a { + text-decoration: none; + color: white; + font-family: 'Montserrat', sans-serif; + font-size: 16px; +} + +nav a:hover { + color: #a8d08d; +} + +nav a#active { + color: tomato; +} + +main { + margin: 0 auto; + padding: 40px 60px; + max-width: 1200px; + flex: 1; +} + +section { + margin: 0 0 50px 0; + padding: 30px; + background-color: #f9f9f9; +} + +h2 { + font-size: 28px; + color: #2d5016; + margin: 0 0 20px 0; +} + +p { + font-family: 'Montserrat', sans-serif; + font-size: 16px; + color: #333; + line-height: 1.6; + margin: 0 0 15px 0; +} + +img { + width: 100%; + height: 400px; + object-fit: cover; + margin: 0 0 20px 0; +} + +ul { + margin: 20px 0; + padding: 0 0 0 25px; +} + +ul li { + font-family: 'Montserrat', sans-serif; + font-size: 16px; + color: #333; + margin: 0 0 12px 0; + line-height: 1.5; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; +} + +table th { + background-color: #2d5016; + color: white; + padding: 15px; + text-align: left; + border: 1px solid #2d5016; + font-size: 16px; +} + +table td { + padding: 12px 15px; + border: 1px solid #ddd; + text-align: left; + font-size: 15px; + color: #333; +} + +table tr:nth-child(even) { + background-color: #f2f2f2; +} + +footer { + background-color: black; + color: white; + padding: 30px 60px; + margin: 0; +} + +footer p { + color: white; + margin: 0 0 10px 0; +} + +footer a { + text-decoration: none; + color: white; + font-family: 'Montserrat', sans-serif; + font-size: 16px; +} + +footer a:hover { + color: #a8d08d; +} + +@media (max-width: 800px) { + nav { + justify-content: space-between; + } + + .about-company img { + width: 500px; + } +} + +@media (max-width: 600px) { + h1 { + text-align: center; + } + + nav { + flex-direction: column; + align-items: center; + } + + nav a { + font-size: 20px; + } + + section h2 { + text-align: center; + } + + .about-company img { + width: 100%; + } + + table th, + table td { + font-size: 14px; + } +} + +@media (max-width: 400px) { + table th, + table td { + font-size: 12px; + } +} \ No newline at end of file diff --git a/labs/lab-08/templates/about.html b/labs/lab-08/templates/about.html new file mode 100644 index 0000000..7cee6eb --- /dev/null +++ b/labs/lab-08/templates/about.html @@ -0,0 +1,42 @@ + + + + + + + О нас - ЭкоЛанч + + + + + + +
+

ЭкоЛанч

+ +
+ +
+
+

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

+

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

+

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

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

ЭкоЛанч

+ +
+ +
+
+

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

+

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

+

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

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

ЭкоЛанч

+ +
+
+
+

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

+
+
+
+ 🍲 +

Суп

+
+
+ 🍽️ +

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

+
+
+ 🥗 +

Салат

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍲 +

Суп

+
+
+ 🍽️ +

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

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍲 +

Суп

+
+
+ 🥗 +

Салат

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍽️ +

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

+
+
+ 🥗 +

Салат

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍽️ +

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

+
+
+ 🥤 +

Напиток

+
+
+ +
+
+ 🍰 +

Десерт

+
+

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

+
+
+

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

+
+ +
+
+

Супы

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

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

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

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

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

Напитки

+
+ + +
+
+
+
+ +
+
+

Десерты

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

ЭкоЛанч

+ +
+ +
+
+

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

+
+ +
+ +
+

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

+
+
+
+

Ваш заказ

+
+

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

+
+
+ +
+

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

+ + + + + + + +
+ + +
+ + + + + + + Доставка осуществляется только по Москве + +
+ +
+ + +
+
+ + +
+
+ + + + Доступное время доставки с 7:00 до 23:00 + + + + +
+ + +
+
+
+
+
+
+ + + + + + \ No newline at end of file