Создание простой книги рецептов на HTML, CSS и JavaScript

Этот проект — отличный учебный кейс для закрепления базовых навыков фронтенд‑разработки: работа с DOM, формами, событиями, простая вёрстка и логика на клиенте. Проект запускается в браузере (Chrome, Firefox и др.) и не требует сервера. Ниже — компактный план и полный набор примеров кода, чек‑листы и советы по расширению.
Что делает этот проект
- Левая колонка: форма добавления рецепта (название, ингредиенты, метод).
- Правая колонка: список рецептов, поиск и возможность удаления.
- Данные хранятся в массиве внутри JavaScript; в разделе улучшений показано сохранение в localStorage.
Важно: в базовой реализации данные не сохраняются между перезагрузками страницы.
Структура проекта (файлы)
- index.html — разметка страницы
- styles.css — стили
- script.js — логика клиента
Ниже — пошаговое создание каждого файла.
index.html — базовая разметка
Создайте файл index.html и вставьте базовую структуру. Этот HTML включает навигацию, контейнер с двумя колонками и подключение CSS/JS.
Recipe App
Добавить рецепт
Список рецептов
У вас нет рецептов.
styles.css — простая вёрстка
Создайте styles.css в той же папке и добавьте базовые стили. Это минимальная стилизация для удобного чтения и адаптивной сетки.
body {
font-family: sans-serif;
margin: 0;
padding: 0;
}
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: 140px 5%;
gap: 20px;
}
.left-column {
width: 25%;
min-width: 260px;
}
.right-column {
width: 65%;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
margin-bottom: 6px;
}
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;
}
#recipe-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
#no-recipes {
display: flex;
background-color: #FFCCCC;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
}
.recipe {
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,.08);
}
.recipe h3 { margin-top: 0; margin-bottom: 10px; }
.recipe ul { margin: 0; padding-left: 18px; }
.recipe ul li { margin-bottom: 5px; }
.delete-button {
background-color: #dc3545;
color: #fff;
border: none;
border-radius: 5px;
padding: 6px 10px;
cursor: pointer;
}
.delete-button:hover { background-color: #c82333; }
@media (max-width: 900px) {
.container { flex-direction: column; margin: 100px 4%; }
.left-column, .right-column { width: 100%; }
}script.js — логика работы (добавление, отображение, удаление, поиск)
Пример базовой реализации поведения приложения. Создавайте файл script.js и вставляйте код ниже.
// Элементы DOM
const form = document.querySelector('#add-recipe-form');
const recipeList = document.querySelector('#recipe-list');
const noRecipes = document.getElementById('no-recipes');
const searchBox = document.getElementById('search-box');
// Хранилище рецептов
let recipes = []; // Можно заменить на загрузку из localStorage при улучшении
// Обработчик отправки формы
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) { alert('Введите название рецепта.'); return; }
if (ingredients.length === 0) { alert('Добавьте минимум один ингредиент.'); return; }
if (!method) { alert('Опишите метод приготовления.'); return; }
// Проверяем, есть ли рецепт с тем же названием
const exists = recipes.some(r => r.name.toLowerCase() === name.toLowerCase());
if (exists) { if (!confirm('Рецепт с таким названием уже существует. Добавить ещё один?')) { return; } }
const newRecipe = { name, ingredients, method };
recipes.push(newRecipe);
// Сброс формы
nameInput.value = '';
ingrInput.value = '';
methodInput.value = '';
// Обновление интерфейса
displayRecipes();
}
// Отображение списка рецептов
function displayRecipes(list = recipes) {
recipeList.innerHTML = '';
list.forEach((recipe, index) => {
const recipeDiv = document.createElement('div');
recipeDiv.classList.add('recipe');
recipeDiv.innerHTML = `
${escapeHtml(recipe.name)}
Ингредиенты:
${recipe.ingredients.map(ingr => `- ${escapeHtml(ingr)}
`).join('')}
Метод:
${escapeHtml(recipe.method)}
`;
recipeList.appendChild(recipeDiv);
});
noRecipes.style.display = recipes.length > 0 ? 'none' : 'flex';
}
// Экранирование HTML для безопасного вывода
function escapeHtml(text) {
if (!text) return '';
return text.replace(/[&<>\"']/g, function (c) {
return {'&':'&','<':'<','>':'>','"':'"','\'':'''}[c];
});
}
// Удаление рецепта
function handleDelete(event) {
if (event.target.classList.contains('delete-button')) {
const index = Number(event.target.dataset.index);
if (!Number.isNaN(index)) {
recipes.splice(index, 1);
// Очистка строки поиска, чтобы показ был свежим
searchBox.value = '';
displayRecipes();
}
}
}
// Поиск по названию
function search(query) {
const q = query.trim().toLowerCase();
if (!q) { displayRecipes(); return; }
const filteredRecipes = recipes.filter(recipe => recipe.name.toLowerCase().includes(q));
displayRecipes(filteredRecipes);
}
// События
form.addEventListener('submit', handleSubmit);
recipeList.addEventListener('click', handleDelete);
searchBox.addEventListener('input', event => search(event.target.value));
// Инициализация (если нужно предварительно отрисовать)
displayRecipes();Улучшение: сохранение в localStorage (опционально)
Чтобы данные не терялись при перезагрузке, сохраняйте массив recipes в localStorage. Пример расширения script.js:
// Загрузка из localStorage при старте
const STORAGE_KEY = 'my_recipe_app_v1';
function saveRecipes() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(recipes)); } catch (e) { console.warn('Не удалось сохранить в localStorage', e); }
}
function loadRecipes() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
recipes = JSON.parse(raw);
}
} catch (e) { console.warn('Ошибка чтения localStorage', e); }
}
// Загрузить при инициализации
loadRecipes();
displayRecipes();
// Вызывать saveRecipes() после добавления и удаления
// Например: после recipes.push(newRecipe) — вызвать saveRecipes();
// и после recipes.splice(...) также вызвать saveRecipes();Совет: используйте версии ключа в STORAGE_KEY при изменении формата данных.
Поиск: UX и нюансы
- Поиск сейчас фильтрует строго по названию. Если нужно, можно расширить фильтрацию по ингредиентам и методу.
- Для больших списков используйте debounce (например, 200–300 мс) при обработке input, чтобы снизить количество вызовов фильтрации.
Критерии приёмки
- Форма добавляет рецепт в массив и очищается после отправки.
- Добавленный рецепт отображается в правой колонке.
- При отсутствии рецептов отображается сообщение об отсутствии.
- Кнопка Delete удаляет соответствующий рецепт и интерфейс обновляется.
- Поиск фильтрует список по введённой строке.
- (Опционально) Данные сохраняются в localStorage и восстанавливаются после перезагрузки.
Тест-кейсы и приёмка
- TC1: Добавление валидного рецепта — проверьте, что рецепт отображается в списке.
- TC2: Попытка добавить пустое название — ожидается сообщение об ошибке/блокировка.
- TC3: Удаление рецепта — рецепт исчез из списка.
- TC4: Поиск по части названия — отображаются только совпадающие рецепты.
- TC5: Перезагрузка страницы без localStorage — список очищен.
- TC6: Сохранение в localStorage — после перезагрузки рецепты остаются.
Чек-листы для ролей
Разработчик:
- Создать index.html, styles.css, script.js
- Реализовать добавление/удаление/отображение/поиск
- Сделать простую валидацию формы
- Подумать о сохранении (localStorage)
Код‑ревьюер / тестировщик:
- Проверить сценарии TC1–TC6
- Проверить кросс‑браузерность (Chrome, Firefox)
- Протестировать поведение при пустых/специальных символах
Когда это решение не подойдёт
- Если нужно централизованное хранение и многопользовательский доступ — требуется сервер (API + база данных).
- Для больших объёмов данных (>1000 записей) клиентская фильтрация и рендеринг потребуют оптимизации (виртуализация, серверная фильтрация).
Альтернативные подходы
- Использовать фреймворки (React, Vue, Svelte) для управления состоянием и компонентным рендерингом.
- Хранить данные на сервере через REST/GraphQL и реализовать авторизацию.
- Использовать IndexedDB, если нужно хранить большие структуры на клиенте.
Мини‑методология: как расширять проект по шагам
- Сделать базовую форму и отображение (MVP).
- Добавить валидацию и UX‑подсказки.
- Реализовать удаление и подтверждения действий.
- Добавить поиск и фильтры (по ингредиентам, тегам).
- Внедрить сохранение (localStorage → IndexedDB → сервер).
- Переписать на компонентный фреймворк при необходимости масштабирования.
Decision flow (простое дерево принятия решений)
flowchart TD
A[Нужен ли доступ с разных устройств?] -->|Да| B[Нужен сервер + API]
A -->|Нет| C[Можно хранить в localStorage]
B --> D[Разработать бэкенд и авторизацию]
C --> E[Добавить export/import или синхронизацию]Безопасность и надежность
- Экранируйте ввод при выводе в HTML (в примере есть escapeHtml).
- Не храните чувствительные данные в localStorage.
- При добавлении серверного API — проверяйте и валидайте данные на сервере.
Шаблон проверки качества UI/UX (коротко)
- Интуитивность: легко найти форму и кнопку отправки.
- Доступность: метки у полей, понятные плейсхолдеры.
- Ошибки: пользователь видит понятные сообщения при неверных данных.
Полезные подсказки и паттерны
- Держите логику маленькими: отдельные функции для валидации, рендера, сохранения.
- Используйте data-* атрибуты для привязки индексов/ID.
- При изменении данных всегда обновляйте и сохранение (если оно есть) и UI.
Резюме
Книга рецептов — компактный проект, который помогает отработать работу с DOM, формами и простой бизнес‑логикой в JavaScript. Начните с базовой реализации, затем улучшайте: добавьте хранение в localStorage, расширьте поиск и реализуйте экспорт/импорт.
Important: сохраняйте код в репозитории и делайте маленькие коммиты — так проще откатываться и тестировать изменения.
Короткие шаги для старта:
- Создать три файла: index.html, styles.css, script.js.
- Вставить базовую разметку, стили и скрипт из этой инструкции.
- Открыть index.html в браузере и проверить добавление/удаление/поиск.
Спасибо — теперь у вас есть рабочая отправная точка для дальнейших экспериментов и улучшений.
Похожие материалы
Восстановление кэша значков в Windows
Стрелки не работают в Excel — быстрое решение
Шифрование USB‑накопителя с VeraCrypt
PowerShell: история команд — просмотр и сохранение
Nandroid — полная резервная копия Android