Рецептная книга: проект на HTML, CSS и JavaScript

О чём эта статья
Задача — сделать фронтенд-приложение, в котором слева пользователь добавляет рецепт, а справа просматривает, удаляет и ищет сохранённые рецепты. Приложение работает в браузере без бэкенда и не сохраняет данные после перезагрузки (в разделе «Альтернативы» есть варианты хранения).
Важно
- Приведённые примеры ориентированы на учебный проект. Они подходят для изучения DOM, событий и базовой валидации форм.
- Код в блоках можно скопировать и запускать локально в любом современном браузере (Chrome, Firefox, Edge, Safari).
Подготовка файлов и базовая разметка
Создайте в одной папке три файла: index.html, styles.css и script.js. Ниже — минимальная HTML-структура с локализованными текстами интерфейса.
Приложение рецептов
Разделите страницу на левую и правую колонку внутри контейнера:
Форма добавления рецепта
В левой колонке разместите форму, где пользователь вводит название, список ингредиентов и способ приготовления. Тексты в примере переведены на русский.
Добавить рецепт
Совет по UX: при вводе ингредиентов предложите пользователю разделять элементы запятой или переносом строки и укажите это в подсказке.
Базовые стили
Добавьте в styles.css простое оформление страницы и колонок:
body {
font-family: sans-serif;
}
nav {
background-color: #333;
position: fixed;
top: 0;
width: 100%;
padding: 20px;
left: 0;
color: white;
text-align: center;
}
.container {
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 150px 5%;
}
.left-column {
width: 25%;
}
.right-column {
width: 65%;
}И стили для самой формы:
form {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 10px;
}
input[type="text"], textarea {
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
border: 1px solid #ccc;
width: 100%;
box-sizing: border-box;
}
button[type="submit"] {
padding: 10px;
background-color: #3338;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}Логика на JavaScript: добавление рецептов
В script.js начните с поиска формы и создания массива для рецептов:
const form = document.querySelector('form');
let recipes = [];Функция обработки отправки формы получает значения, валидирует их и добавляет в массив. В примере ингредиенты разбиваются по запятой и очищаются от пробелов.
function handleSubmit(event) {
// Предотвратить отправку формы и перезагрузку страницы
event.preventDefault();
// Получаем элементы формы
const nameInput = document.querySelector('#recipe-name');
const ingrInput = document.querySelector('#recipe-ingredients');
const methodInput = document.querySelector('#recipe-method');
const name = nameInput.value.trim();
const ingredients = ingrInput.value.trim().split(',').map(i => i.trim()).filter(Boolean);
const method = methodInput.value.trim();
// Простая проверка валидности
if (name && ingredients.length > 0 && method) {
const newRecipe = { name, ingredients, method };
recipes.push(newRecipe);
// Очистить форму
nameInput.value = '';
ingrInput.value = '';
methodInput.value = '';
// Обновить отображение
displayRecipes();
} else {
// Можно показывать пользовательское уведомление об ошибке
alert('Пожалуйста, заполните все поля и укажите хотя бы один ингредиент.');
}
}
form.addEventListener('submit', handleSubmit);Пояснение терминов
- DOM: модель документа, с которой работает JavaScript для поиска и изменения элементов страницы.
Отображение списка рецептов
В правой колонке добавьте контейнер для списка рецептов и сообщение, если список пуст:
Список рецептов
У вас нет рецептов.
CSS для списка и сообщения об отсутствии записей:
#recipe-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 20px;
}
#no-recipes {
display: flex;
background-color: #FFCCCC;
padding: 20px;
border-radius: 8px;
margin-top: 44px;
}Функция отображения итеративно создаёт элементы для каждого рецепта:
const recipeList = document.querySelector('#recipe-list');
const noRecipes = document.getElementById('no-recipes');
function displayRecipes() {
recipeList.innerHTML = '';
recipes.forEach((recipe, index) => {
const recipeDiv = document.createElement('div');
recipeDiv.classList.add('recipe');
recipeDiv.innerHTML = `
${recipe.name}
Ингредиенты:
${recipe.ingredients.map(ingr => `- ${ingr}
`).join('')}
Способ:
${recipe.method}
`;
recipeList.appendChild(recipeDiv);
});
noRecipes.style.display = recipes.length > 0 ? 'none' : 'flex';
}Стили для карточки рецепта:
.recipe {
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,.2);
}
.recipe h3 {
margin-top: 0;
margin-bottom: 10px;
}
.recipe ul {
margin: 0;
padding: 0;
list-style: none;
}
.recipe ul li {
margin-bottom: 5px;
}Удаление рецептов
Добавьте стили для кнопки удаления:
.delete-button {
background-color: #dc3545;
color: #fff;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
}
.delete-button:hover {
background-color: #c82333;
}Логика удаления опирается на делегирование событий: один обработчик на контейнере списка удаляет элемент по его индексу.
function handleDelete(event) {
if (event.target.classList.contains('delete-button')) {
const index = parseInt(event.target.dataset.index, 10);
if (!Number.isNaN(index)) {
recipes.splice(index, 1);
// Очистим поле поиска, чтобы вернуть полный список
const searchBox = document.getElementById('search-box');
if (searchBox) searchBox.value = '';
displayRecipes();
}
}
}
recipeList.addEventListener('click', handleDelete);Важно: при удалении по индексу, если вы используете фильтр (search), индекс может не совпадать с исходным положением в массиве. В следующем разделе показано, как корректно обрабатывать удаление в режиме поиска.
Поиск рецептов
В элементе поиска мы фильтруем массив recipes по названию и рисуем отфильтрованные карточки. Элемент поиска можно добавить в начало правой колонки (см. выше).
const searchBox = document.getElementById('search-box');
function search(query) {
const filteredRecipes = recipes.filter(recipe => {
return recipe.name.toLowerCase().includes(query.toLowerCase());
});
recipeList.innerHTML = '';
filteredRecipes.forEach(recipe => {
const recipeEl = document.createElement('div');
recipeEl.classList.add('recipe');
recipeEl.innerHTML = `
${recipe.name}
Ингредиенты:
${recipe.ingredients.map(ingr => `- ${ingr}
`).join('')}
Способ:
${recipe.method}
`;
recipeList.appendChild(recipeEl);
});
noRecipes.style.display = (filteredRecipes.length > 0) ? 'none' : 'flex';
}
searchBox.addEventListener('input', event => search(event.target.value));Замечание: в этом простом примере при отображении отфильтрованных результатов мы используем recipes.indexOf(recipe) для установки data-index кнопки удаления. Это работает при уникальных объектах, но если в массиве одинаковые объекты или идентичные строки — лучше хранить уникальный id для каждого рецепта.
Улучшения и альтернативы
Когда этот подход не работает
- Если нужно сохранять данные между перезагрузками — текущий массив в памяти не подойдёт.
- Если много данных или нужно совместное использование — потребуется бэкенд и база данных.
Альтернативные подходы
- localStorage: простой способ сохранить рецепты в браузере между сессиями. Подходит для одиночного пользователя.
- IndexedDB: для более сложных локальных хранилищ и больших наборов данных.
- Бэкенд (REST/GraphQL): для мультипользовательских приложений с авторизацией и хранением на сервере.
Мини-методология разработки
- Прототип на бумаге: определите поля рецепта и базовую навигацию.
- Верстка: статический HTML/CSS без логики.
- JavaScript: добавить обработку форм и отображение.
- Тесты UX: проверка добавления, поиска, удаления.
- Улучшение: localStorage, валидация, уникальные id.
Чек-листы по ролям
Разработчик интерфейса:
- Верстка адаптивна
- Поля формы валидируются
- Кнопки имеют состояния hover/focus
Разработчик логики:
- Корректно добавляются объекты в массив
- Удаление не ломает индексы
- Поиск нечувствителен к регистру
Тестировщик:
- Добавить рецепт с пустыми полями — ожидание ошибки
- Удалить рецепт при активном поиске — список обновляется
Шаблоны и сниппеты
- Генерация уникального id (простая функция):
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}Использование id при добавлении рецепта:
const newRecipe = { id: uid(), name, ingredients, method };- Сохранение и загрузка из localStorage:
function saveRecipes() {
localStorage.setItem('recipes', JSON.stringify(recipes));
}
function loadRecipes() {
const raw = localStorage.getItem('recipes');
if (raw) recipes = JSON.parse(raw);
}
// Вызывать loadRecipes() при инициализации и saveRecipes() после измененийСовместимость и миграция
- Код ориентирован на современные браузеры с поддержкой ES6. Для поддержки старых браузеров потребуется транспиляция (Babel) и полифилы.
- localStorage доступен в большинстве десктопных и мобильных браузеров, но не в режимах приватного просмотра некоторых браузеров.
Локальные особенности для русскоязычных пользователей
- Формат ингредиентов и чисел: используйте запятые и точки по принятому в вашей целевой аудитории стилю.
- Подсказки и placeholder-ы делайте на русском, чтобы пользователи понимали способ ввода.
Критерии приёмки
- Пользователь может добавить рецепт с названием, списком ингредиентов и способом.
- После добавления рецепт отображается в правой колонке.
- Пользователь может удалить рецепт, и список обновляется.
- Поиск фильтрует список по названию (без учёта регистра).
- Интерфейс корректно отображается на экранах от 320px и выше (адаптивность).
Безопасность и приватность
- Приложение не отправляет данные на сервер по умолчанию. Если вы добавляете синхронизацию с сервером, обязательно используйте HTTPS.
- При использовании localStorage помните, что данные хранятся на устройстве и доступны другим скриптам в том же домене.
Однострочный глоссарий
- localStorage: браузерное key/value-хранилище для сохранения данных между сессиями.
- DOM: объектная модель документа, доступная для изменения через JavaScript.
Примеры ошибок и как их исправлять
Проблема: при фильтрации удаление удаляет не тот элемент. Решение: присваивайте каждому рецепту уникальный id и используйте его для удаления.
Проблема: после перезагрузки данные исчезают. Решение: сохраняйте recipes в localStorage и загружайте при старте.
Краткое резюме
- Проект «Рецептная книга» — отличный способ попрактиковаться в HTML, CSS и JavaScript.
- Базовый функционал: добавление, отображение, удаление и поиск рецептов.
- Рекомендации по развитию: добавить уникальные id, сохранение в localStorage или бэкенд, а также улучшить валидацию и UX.
Дополнительные изображения






Итог
Этот проект даёт практику работы с формами, событиями и динамическим отображением данных в DOM. После реализации базовой версии вы можете расширять функционал: добавлять редактирование рецептов, теги, сортировку и облачную синхронизацию.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone