Add 18 new recipes and update index + lemon pasta
This commit is contained in:
@@ -397,6 +397,88 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Step-by-step instruction input */
|
||||
.steps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.step-input-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--terracotta);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Playfair Display', serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.step-input-row textarea {
|
||||
flex: 1;
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.step-remove-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: 1.5px solid rgba(107, 101, 96, 0.2);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: var(--warm-gray);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.step-remove-btn:hover {
|
||||
background: var(--terracotta);
|
||||
border-color: var(--terracotta);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-step-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: transparent;
|
||||
border: 2px dashed rgba(107, 101, 96, 0.25);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-family: 'Source Sans 3', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--warm-gray);
|
||||
transition: all 0.2s;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.add-step-btn:hover {
|
||||
border-color: var(--olive);
|
||||
color: var(--olive);
|
||||
background: rgba(92, 107, 74, 0.05);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -406,6 +488,120 @@
|
||||
border-top: 1px solid rgba(107, 101, 96, 0.1);
|
||||
}
|
||||
|
||||
/* Tag checkboxes */
|
||||
.tags-input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-checkbox {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-checkbox input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tag-pill {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1.5px solid rgba(107, 101, 96, 0.25);
|
||||
border-radius: 50px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--warm-gray);
|
||||
transition: all 0.2s;
|
||||
background: var(--warm-white);
|
||||
}
|
||||
|
||||
.tag-checkbox input:checked + .tag-pill {
|
||||
background: var(--olive);
|
||||
border-color: var(--olive);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tag-checkbox:hover .tag-pill {
|
||||
border-color: var(--olive);
|
||||
}
|
||||
|
||||
/* Recipe card tags */
|
||||
.recipe-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--olive);
|
||||
font-weight: 600;
|
||||
background: rgba(92, 107, 74, 0.1);
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
/* Filter bar */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--warm-gray);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
padding: 0.5rem 1.25rem;
|
||||
border: 1.5px solid rgba(107, 101, 96, 0.2);
|
||||
border-radius: 50px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--warm-gray);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: 'Source Sans 3', sans-serif;
|
||||
}
|
||||
|
||||
.filter-tag:hover {
|
||||
border-color: var(--olive);
|
||||
color: var(--olive);
|
||||
}
|
||||
|
||||
.filter-tag.active {
|
||||
background: var(--olive);
|
||||
border-color: var(--olive);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
font-size: 0.85rem;
|
||||
color: var(--warm-gray);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-family: 'Source Sans 3', sans-serif;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
color: var(--terracotta);
|
||||
}
|
||||
|
||||
/* View Modal */
|
||||
.view-modal .modal {
|
||||
max-width: 700px;
|
||||
@@ -417,13 +613,22 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-category {
|
||||
font-size: 0.75rem;
|
||||
.view-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.view-tag {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--olive);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
background: rgba(92, 107, 74, 0.12);
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.view-title {
|
||||
@@ -447,6 +652,8 @@
|
||||
gap: 2rem;
|
||||
padding: 1.5rem 2.5rem;
|
||||
border-bottom: 1px dashed rgba(107, 101, 96, 0.15);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
@@ -468,6 +675,60 @@
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Serving Scaler */
|
||||
.serving-scaler {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: var(--paper);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 50px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.serving-scaler label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--warm-gray);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scaler-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: var(--warm-white);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: var(--charcoal);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.scaler-btn:hover {
|
||||
background: var(--terracotta);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.scaler-value {
|
||||
font-family: 'Playfair Display', serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--charcoal);
|
||||
min-width: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scaled-amount {
|
||||
color: var(--terracotta);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.view-section {
|
||||
padding: 2rem 2.5rem;
|
||||
}
|
||||
@@ -595,6 +856,14 @@
|
||||
<button class="btn btn-primary" onclick="openAddModal()">+ Add Recipe</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar" id="filterBar" style="display:none">
|
||||
<span class="filter-label">Filter:</span>
|
||||
<div class="filter-tags" id="filterTags">
|
||||
<!-- Tags will be generated dynamically -->
|
||||
</div>
|
||||
<button class="filter-clear" id="clearFilters" onclick="clearFilters()" style="display:none">Clear</button>
|
||||
</div>
|
||||
|
||||
<div class="recipes-grid" id="recipesGrid">
|
||||
<!-- Recipes will be loaded here -->
|
||||
</div>
|
||||
@@ -618,16 +887,25 @@
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="category">Category</label>
|
||||
<select id="category">
|
||||
<option value="Breakfast">Breakfast</option>
|
||||
<option value="Lunch">Lunch</option>
|
||||
<option value="Dinner">Dinner</option>
|
||||
<option value="Dessert">Dessert</option>
|
||||
<option value="Appetizer">Appetizer</option>
|
||||
<option value="Snack">Snack</option>
|
||||
<option value="Beverage">Beverage</option>
|
||||
</select>
|
||||
<label>Tags</label>
|
||||
<div class="tags-input" id="tagsInput">
|
||||
<label class="tag-checkbox">
|
||||
<input type="checkbox" name="tags" value="Breakfast">
|
||||
<span class="tag-pill">Breakfast</span>
|
||||
</label>
|
||||
<label class="tag-checkbox">
|
||||
<input type="checkbox" name="tags" value="Lunch">
|
||||
<span class="tag-pill">Lunch</span>
|
||||
</label>
|
||||
<label class="tag-checkbox">
|
||||
<input type="checkbox" name="tags" value="Dinner">
|
||||
<span class="tag-pill">Dinner</span>
|
||||
</label>
|
||||
<label class="tag-checkbox">
|
||||
<input type="checkbox" name="tags" value="Snack">
|
||||
<span class="tag-pill">Snack</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="servings">Servings</label>
|
||||
@@ -657,8 +935,13 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="instructions">Instructions (one step per line)</label>
|
||||
<textarea id="instructions" rows="6" placeholder="Preheat oven to 350°F Mix dry ingredients Add wet ingredients" required></textarea>
|
||||
<label>Instructions</label>
|
||||
<div class="steps-container" id="stepsContainer">
|
||||
<!-- Steps will be added dynamically -->
|
||||
</div>
|
||||
<button type="button" class="add-step-btn" onclick="addStep()">
|
||||
<span>+</span> Add Step
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
@@ -675,7 +958,7 @@
|
||||
<div class="modal">
|
||||
<button class="close-btn" onclick="closeModal('viewModal')" style="position:absolute;top:1rem;right:1rem;z-index:10">×</button>
|
||||
<div class="view-header">
|
||||
<p class="view-category" id="viewCategory"></p>
|
||||
<div class="view-tags" id="viewTags"></div>
|
||||
<h2 class="view-title" id="viewTitle"></h2>
|
||||
<p class="view-description" id="viewDescription"></p>
|
||||
</div>
|
||||
@@ -701,6 +984,11 @@
|
||||
<script>
|
||||
let recipes = [];
|
||||
let currentRecipeId = null;
|
||||
let currentServings = 1;
|
||||
let originalServings = 1;
|
||||
let originalIngredients = [];
|
||||
let activeFilters = new Set();
|
||||
let allTags = new Set();
|
||||
|
||||
// Load recipes on page load
|
||||
document.addEventListener('DOMContentLoaded', loadRecipes);
|
||||
@@ -709,32 +997,105 @@
|
||||
try {
|
||||
const response = await fetch('/api/recipes');
|
||||
recipes = await response.json();
|
||||
buildFilterTags();
|
||||
renderRecipes();
|
||||
} catch (error) {
|
||||
showToast('Failed to load recipes');
|
||||
}
|
||||
}
|
||||
|
||||
function buildFilterTags() {
|
||||
// Collect all unique tags from all recipes
|
||||
allTags.clear();
|
||||
recipes.forEach(recipe => {
|
||||
(recipe.tags || []).forEach(tag => allTags.add(tag));
|
||||
});
|
||||
|
||||
const filterBar = document.getElementById('filterBar');
|
||||
const filterTagsContainer = document.getElementById('filterTags');
|
||||
|
||||
// Hide filter bar if no tags exist
|
||||
if (allTags.size === 0) {
|
||||
filterBar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
filterBar.style.display = 'flex';
|
||||
|
||||
// Sort tags alphabetically for consistency
|
||||
const sortedTags = Array.from(allTags).sort();
|
||||
|
||||
// Generate filter buttons
|
||||
filterTagsContainer.innerHTML = sortedTags.map(tag => {
|
||||
const isActive = activeFilters.has(tag) ? 'active' : '';
|
||||
return `<button class="filter-tag ${isActive}" data-tag="${tag}" onclick="toggleFilter('${tag}')">${tag}</button>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function toggleFilter(tag) {
|
||||
const btn = document.querySelector(`.filter-tag[data-tag="${tag}"]`);
|
||||
if (activeFilters.has(tag)) {
|
||||
activeFilters.delete(tag);
|
||||
btn.classList.remove('active');
|
||||
} else {
|
||||
activeFilters.add(tag);
|
||||
btn.classList.add('active');
|
||||
}
|
||||
|
||||
document.getElementById('clearFilters').style.display =
|
||||
activeFilters.size > 0 ? 'inline' : 'none';
|
||||
|
||||
renderRecipes();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
activeFilters.clear();
|
||||
document.querySelectorAll('.filter-tag').forEach(btn => btn.classList.remove('active'));
|
||||
document.getElementById('clearFilters').style.display = 'none';
|
||||
renderRecipes();
|
||||
}
|
||||
|
||||
function getFilteredRecipes() {
|
||||
if (activeFilters.size === 0) return recipes;
|
||||
|
||||
return recipes.filter(recipe => {
|
||||
const recipeTags = recipe.tags || [];
|
||||
// Recipe must have at least one of the active filter tags
|
||||
return recipeTags.some(tag => activeFilters.has(tag));
|
||||
});
|
||||
}
|
||||
|
||||
function renderRecipes() {
|
||||
const grid = document.getElementById('recipesGrid');
|
||||
const count = document.getElementById('recipeCount');
|
||||
count.textContent = recipes.length;
|
||||
const filteredRecipes = getFilteredRecipes();
|
||||
|
||||
count.textContent = filteredRecipes.length;
|
||||
|
||||
if (recipes.length === 0) {
|
||||
if (filteredRecipes.length === 0) {
|
||||
const message = activeFilters.size > 0
|
||||
? 'No recipes match your filters'
|
||||
: 'Your recipe box is empty';
|
||||
const submessage = activeFilters.size > 0
|
||||
? 'Try removing some filters or add a new recipe!'
|
||||
: 'Add your first recipe to get started!';
|
||||
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📖</div>
|
||||
<h3>Your recipe box is empty</h3>
|
||||
<p>Add your first recipe to get started!</p>
|
||||
<h3>${message}</h3>
|
||||
<p>${submessage}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = recipes.map(recipe => `
|
||||
grid.innerHTML = filteredRecipes.map(recipe => `
|
||||
<article class="recipe-card" onclick="viewRecipe('${recipe.id}')">
|
||||
<div class="card-header">
|
||||
<p class="recipe-category">${recipe.category || 'Uncategorized'}</p>
|
||||
<div class="recipe-tags">
|
||||
${(recipe.tags || []).map(tag => `<span class="recipe-tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
<h3 class="recipe-title">${recipe.title}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -748,13 +1109,199 @@
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Parse serving number from string like "4-6" or "4 servings" or just "4"
|
||||
function parseServings(servingsStr) {
|
||||
if (!servingsStr) return 4;
|
||||
const match = servingsStr.match(/(\d+)/);
|
||||
return match ? parseInt(match[1]) : 4;
|
||||
}
|
||||
|
||||
// Parse amount from ingredient string
|
||||
function parseIngredientAmount(ingredient) {
|
||||
// Match fractions, decimals, and whole numbers at the start
|
||||
const fractionMap = {
|
||||
'½': 0.5, '⅓': 0.333, '⅔': 0.667, '¼': 0.25, '¾': 0.75,
|
||||
'⅛': 0.125, '⅜': 0.375, '⅝': 0.625, '⅞': 0.875
|
||||
};
|
||||
|
||||
let text = ingredient.trim();
|
||||
let amount = null;
|
||||
let rest = text;
|
||||
|
||||
// Check for unicode fractions
|
||||
for (const [frac, val] of Object.entries(fractionMap)) {
|
||||
if (text.startsWith(frac)) {
|
||||
amount = val;
|
||||
rest = text.slice(1).trim();
|
||||
break;
|
||||
}
|
||||
// Check for "1½" style
|
||||
const mixedMatch = text.match(/^(\d+)\s*([½⅓⅔¼¾⅛⅜⅝⅞])/);
|
||||
if (mixedMatch) {
|
||||
amount = parseInt(mixedMatch[1]) + fractionMap[mixedMatch[2]];
|
||||
rest = text.slice(mixedMatch[0].length).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for "1/2" style fractions
|
||||
if (amount === null) {
|
||||
const fracMatch = text.match(/^(\d+)\s*\/\s*(\d+)/);
|
||||
if (fracMatch) {
|
||||
amount = parseInt(fracMatch[1]) / parseInt(fracMatch[2]);
|
||||
rest = text.slice(fracMatch[0].length).trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for mixed "1 1/2" style
|
||||
if (amount === null) {
|
||||
const mixedFracMatch = text.match(/^(\d+)\s+(\d+)\s*\/\s*(\d+)/);
|
||||
if (mixedFracMatch) {
|
||||
amount = parseInt(mixedFracMatch[1]) + parseInt(mixedFracMatch[2]) / parseInt(mixedFracMatch[3]);
|
||||
rest = text.slice(mixedFracMatch[0].length).trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for decimal or whole number
|
||||
if (amount === null) {
|
||||
const numMatch = text.match(/^(\d+\.?\d*)/);
|
||||
if (numMatch) {
|
||||
amount = parseFloat(numMatch[1]);
|
||||
rest = text.slice(numMatch[0].length).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return { amount, rest };
|
||||
}
|
||||
|
||||
// Format a number nicely (fractions where sensible)
|
||||
function formatAmount(num) {
|
||||
if (num === null) return null;
|
||||
|
||||
const fractions = [
|
||||
{ val: 0.125, str: '⅛' }, { val: 0.25, str: '¼' }, { val: 0.333, str: '⅓' },
|
||||
{ val: 0.375, str: '⅜' }, { val: 0.5, str: '½' }, { val: 0.625, str: '⅝' },
|
||||
{ val: 0.667, str: '⅔' }, { val: 0.75, str: '¾' }, { val: 0.875, str: '⅞' }
|
||||
];
|
||||
|
||||
const whole = Math.floor(num);
|
||||
const decimal = num - whole;
|
||||
|
||||
// Find closest fraction
|
||||
let fracStr = '';
|
||||
if (decimal > 0.05) {
|
||||
const closest = fractions.reduce((prev, curr) =>
|
||||
Math.abs(curr.val - decimal) < Math.abs(prev.val - decimal) ? curr : prev
|
||||
);
|
||||
if (Math.abs(closest.val - decimal) < 0.1) {
|
||||
fracStr = closest.str;
|
||||
} else {
|
||||
// Just use decimal
|
||||
return num % 1 === 0 ? num.toString() : num.toFixed(1).replace(/\.0$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
if (whole === 0 && fracStr) return fracStr;
|
||||
if (fracStr) return `${whole}${fracStr}`;
|
||||
return whole.toString();
|
||||
}
|
||||
|
||||
// Scale ingredients based on serving ratio
|
||||
function scaleIngredients(ingredients, ratio) {
|
||||
return ingredients.map(ing => {
|
||||
const { amount, rest } = parseIngredientAmount(ing);
|
||||
if (amount !== null) {
|
||||
const scaledAmount = amount * ratio;
|
||||
const formattedAmount = formatAmount(scaledAmount);
|
||||
return `<span class="scaled-amount">${formattedAmount}</span> ${rest}`;
|
||||
}
|
||||
return ing;
|
||||
});
|
||||
}
|
||||
|
||||
function updateIngredientDisplay() {
|
||||
const ratio = currentServings / originalServings;
|
||||
const scaledIngredients = scaleIngredients(originalIngredients, ratio);
|
||||
document.getElementById('viewIngredients').innerHTML =
|
||||
scaledIngredients.map(i => `<li>${i}</li>`).join('');
|
||||
document.getElementById('scalerValue').textContent = currentServings;
|
||||
}
|
||||
|
||||
function adjustServings(delta) {
|
||||
const newServings = currentServings + delta;
|
||||
if (newServings >= 1 && newServings <= 99) {
|
||||
currentServings = newServings;
|
||||
updateIngredientDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Add New Recipe';
|
||||
document.getElementById('recipeForm').reset();
|
||||
document.getElementById('recipeId').value = '';
|
||||
// Reset tag checkboxes
|
||||
document.querySelectorAll('input[name="tags"]').forEach(cb => cb.checked = false);
|
||||
// Reset steps with one empty step
|
||||
document.getElementById('stepsContainer').innerHTML = '';
|
||||
addStep();
|
||||
openModal('formModal');
|
||||
}
|
||||
|
||||
// Step management
|
||||
let stepCounter = 0;
|
||||
|
||||
function addStep(content = '') {
|
||||
stepCounter++;
|
||||
const container = document.getElementById('stepsContainer');
|
||||
const stepNum = container.children.length + 1;
|
||||
|
||||
const stepDiv = document.createElement('div');
|
||||
stepDiv.className = 'step-input-row';
|
||||
stepDiv.dataset.stepId = stepCounter;
|
||||
stepDiv.innerHTML = `
|
||||
<span class="step-number">${stepNum}</span>
|
||||
<textarea placeholder="Describe this step..." required>${content}</textarea>
|
||||
<button type="button" class="step-remove-btn" onclick="removeStep(${stepCounter})">×</button>
|
||||
`;
|
||||
container.appendChild(stepDiv);
|
||||
|
||||
// Focus the new textarea
|
||||
stepDiv.querySelector('textarea').focus();
|
||||
}
|
||||
|
||||
function removeStep(stepId) {
|
||||
const container = document.getElementById('stepsContainer');
|
||||
const steps = container.querySelectorAll('.step-input-row');
|
||||
|
||||
// Don't remove if it's the only step
|
||||
if (steps.length <= 1) {
|
||||
showToast('Need at least one step!');
|
||||
return;
|
||||
}
|
||||
|
||||
const stepToRemove = container.querySelector(`[data-step-id="${stepId}"]`);
|
||||
if (stepToRemove) {
|
||||
stepToRemove.remove();
|
||||
renumberSteps();
|
||||
}
|
||||
}
|
||||
|
||||
function renumberSteps() {
|
||||
const container = document.getElementById('stepsContainer');
|
||||
const steps = container.querySelectorAll('.step-input-row');
|
||||
steps.forEach((step, index) => {
|
||||
step.querySelector('.step-number').textContent = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function getStepsContent() {
|
||||
const container = document.getElementById('stepsContainer');
|
||||
const textareas = container.querySelectorAll('textarea');
|
||||
return Array.from(textareas)
|
||||
.map(ta => ta.value.trim())
|
||||
.filter(content => content.length > 0);
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
document.getElementById(id).classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -768,16 +1315,26 @@
|
||||
async function saveRecipe(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const instructions = getStepsContent();
|
||||
if (instructions.length === 0) {
|
||||
showToast('Add at least one instruction step!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected tags
|
||||
const tagCheckboxes = document.querySelectorAll('input[name="tags"]:checked');
|
||||
const tags = Array.from(tagCheckboxes).map(cb => cb.value);
|
||||
|
||||
const id = document.getElementById('recipeId').value;
|
||||
const recipe = {
|
||||
title: document.getElementById('title').value,
|
||||
category: document.getElementById('category').value,
|
||||
tags: tags,
|
||||
servings: document.getElementById('servings').value,
|
||||
prepTime: document.getElementById('prepTime').value,
|
||||
cookTime: document.getElementById('cookTime').value,
|
||||
description: document.getElementById('description').value,
|
||||
ingredients: document.getElementById('ingredients').value.split('\n').filter(i => i.trim()),
|
||||
instructions: document.getElementById('instructions').value.split('\n').filter(i => i.trim())
|
||||
instructions: instructions
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -805,21 +1362,35 @@
|
||||
if (!recipe) return;
|
||||
|
||||
currentRecipeId = id;
|
||||
originalServings = parseServings(recipe.servings);
|
||||
currentServings = originalServings;
|
||||
originalIngredients = recipe.ingredients || [];
|
||||
|
||||
document.getElementById('viewCategory').textContent = recipe.category || 'Uncategorized';
|
||||
// Tags
|
||||
document.getElementById('viewTags').innerHTML =
|
||||
(recipe.tags || []).map(tag => `<span class="view-tag">${tag}</span>`).join('');
|
||||
|
||||
document.getElementById('viewTitle').textContent = recipe.title;
|
||||
document.getElementById('viewDescription').textContent = recipe.description || '';
|
||||
|
||||
// Meta
|
||||
// Meta with serving scaler
|
||||
let metaHtml = '';
|
||||
if (recipe.prepTime) metaHtml += `<div class="meta-item"><p class="meta-value">${recipe.prepTime}</p><p class="meta-label">Prep Time</p></div>`;
|
||||
if (recipe.cookTime) metaHtml += `<div class="meta-item"><p class="meta-value">${recipe.cookTime}</p><p class="meta-label">Cook Time</p></div>`;
|
||||
if (recipe.servings) metaHtml += `<div class="meta-item"><p class="meta-value">${recipe.servings}</p><p class="meta-label">Servings</p></div>`;
|
||||
|
||||
// Always show serving scaler
|
||||
metaHtml += `
|
||||
<div class="serving-scaler">
|
||||
<label>Servings</label>
|
||||
<button class="scaler-btn" onclick="adjustServings(-1)">−</button>
|
||||
<span class="scaler-value" id="scalerValue">${currentServings}</span>
|
||||
<button class="scaler-btn" onclick="adjustServings(1)">+</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('viewMeta').innerHTML = metaHtml;
|
||||
|
||||
// Ingredients
|
||||
document.getElementById('viewIngredients').innerHTML =
|
||||
(recipe.ingredients || []).map(i => `<li>${i}</li>`).join('');
|
||||
// Ingredients (will be scaled)
|
||||
updateIngredientDisplay();
|
||||
|
||||
// Instructions
|
||||
document.getElementById('viewInstructions').innerHTML =
|
||||
@@ -837,13 +1408,27 @@
|
||||
document.getElementById('modalTitle').textContent = 'Edit Recipe';
|
||||
document.getElementById('recipeId').value = recipe.id;
|
||||
document.getElementById('title').value = recipe.title;
|
||||
document.getElementById('category').value = recipe.category || 'Dinner';
|
||||
|
||||
// Set tag checkboxes
|
||||
document.querySelectorAll('input[name="tags"]').forEach(cb => {
|
||||
cb.checked = (recipe.tags || []).includes(cb.value);
|
||||
});
|
||||
|
||||
document.getElementById('servings').value = recipe.servings || '';
|
||||
document.getElementById('prepTime').value = recipe.prepTime || '';
|
||||
document.getElementById('cookTime').value = recipe.cookTime || '';
|
||||
document.getElementById('description').value = recipe.description || '';
|
||||
document.getElementById('ingredients').value = (recipe.ingredients || []).join('\n');
|
||||
document.getElementById('instructions').value = (recipe.instructions || []).join('\n');
|
||||
|
||||
// Populate step inputs
|
||||
const stepsContainer = document.getElementById('stepsContainer');
|
||||
stepsContainer.innerHTML = '';
|
||||
const instructions = recipe.instructions || [];
|
||||
if (instructions.length === 0) {
|
||||
addStep();
|
||||
} else {
|
||||
instructions.forEach(step => addStep(step));
|
||||
}
|
||||
|
||||
openModal('formModal');
|
||||
}
|
||||
@@ -856,6 +1441,12 @@
|
||||
if (response.ok) {
|
||||
closeModal('viewModal');
|
||||
await loadRecipes();
|
||||
// Clean up any filters for tags that no longer exist
|
||||
activeFilters.forEach(tag => {
|
||||
if (!allTags.has(tag)) {
|
||||
activeFilters.delete(tag);
|
||||
}
|
||||
});
|
||||
showToast('Recipe deleted');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -871,4 +1462,4 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user