Add 18 new recipes and update index + lemon pasta
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules/
|
||||
.claude/
|
||||
@@ -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 (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!';
|
||||
|
||||
if (recipes.length === 0) {
|
||||
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 || [];
|
||||
|
||||
// Tags
|
||||
document.getElementById('viewTags').innerHTML =
|
||||
(recipe.tags || []).map(tag => `<span class="view-tag">${tag}</span>`).join('');
|
||||
|
||||
document.getElementById('viewCategory').textContent = recipe.category || 'Uncategorized';
|
||||
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) {
|
||||
|
||||
34
recipes/baked-oatmeal.json
Normal file
34
recipes/baked-oatmeal.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Baked Oatmeal",
|
||||
"tags": [
|
||||
"Breakfast"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "35 mins",
|
||||
"description": "Slice it like a cake, reheat a portion each morning, feel like you have your life together. Very cozy cottagecore energy, very practical reality.",
|
||||
"ingredients": [
|
||||
"300g rolled oats",
|
||||
"480ml whole milk",
|
||||
"2 large eggs",
|
||||
"60ml maple syrup",
|
||||
"3 tablespoons melted butter",
|
||||
"2 teaspoons vanilla extract",
|
||||
"1 teaspoon baking powder",
|
||||
"1 teaspoon cinnamon",
|
||||
"1 pinch salt",
|
||||
"150g blueberries or sliced banana (or both)",
|
||||
"Cooking spray for greasing"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 190°C / 375°F. Grease a medium baking dish (roughly 20x20cm) with cooking spray.",
|
||||
"In a large bowl, whisk together milk, eggs, maple syrup, melted butter, and vanilla extract.",
|
||||
"Stir in rolled oats, baking powder, cinnamon, and salt until fully combined.",
|
||||
"Fold in your fruit of choice.",
|
||||
"Pour the mixture into the prepared baking dish and spread evenly.",
|
||||
"Bake for 35 minutes until set, lightly golden on top, and pulling away from the edges slightly.",
|
||||
"Cool, then slice into portions. Store in the fridge for up to 5 days. Reheat individual slices in the microwave for 60-90 seconds or in the oven at 160°C for 10 minutes."
|
||||
],
|
||||
"id": "baked-oatmeal",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
26
recipes/banana-oat-cookies.json
Normal file
26
recipes/banana-oat-cookies.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"title": "3-Ingredient Banana Oat Cookies",
|
||||
"tags": [
|
||||
"Breakfast",
|
||||
"Snack"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "5 mins",
|
||||
"cookTime": "15 mins",
|
||||
"description": "Three ingredients, zero guilt, maximum kid approval. Naturally sweet, soft, and ready in 20 minutes flat.",
|
||||
"ingredients": [
|
||||
"3 ripe bananas",
|
||||
"200g rolled oats",
|
||||
"60g chocolate chips or raisins"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 180°C / 350°F. Line a baking sheet with parchment paper.",
|
||||
"Peel bananas and mash in a large bowl until completely smooth — the riper they are, the sweeter the cookies.",
|
||||
"Stir rolled oats into the mashed banana until fully combined. Fold in chocolate chips or raisins.",
|
||||
"Drop heaped tablespoons of mixture onto the baking sheet and flatten slightly with the back of a spoon.",
|
||||
"Bake for 12-15 minutes until golden on the bottom and set on top.",
|
||||
"Let cool completely on the tray. Store in an airtight container in the fridge for up to 5 days, or freeze for up to 2 months."
|
||||
],
|
||||
"id": "banana-oat-cookies",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
37
recipes/beef-bourguignon-housewife-flex.json
Normal file
37
recipes/beef-bourguignon-housewife-flex.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"title": "Beef Bourguignon (The Housewife Flex Dish)",
|
||||
"tags": [
|
||||
"Dinner"
|
||||
],
|
||||
"servings": "6",
|
||||
"prepTime": "30 mins",
|
||||
"cookTime": "3 hrs 30 mins",
|
||||
"description": "A rich, wine-braised French beef stew that screams 'I have my life together.' Serves 6-8 and will absolutely DESTROY at dinner.",
|
||||
"ingredients": [
|
||||
"1.5kg beef chuck, cut into 5cm cubes",
|
||||
"750ml dry red wine — 1 bottle (Burgundy or Pinot Noir)",
|
||||
"500ml beef stock (1 standard carton)",
|
||||
"200g thick-cut bacon lardons (1 standard pack)",
|
||||
"300g pearl onions, peeled (1 net bag)",
|
||||
"300g cremini mushrooms, halved (1 punnet)",
|
||||
"3 carrots, roughly chopped",
|
||||
"4 garlic cloves, minced",
|
||||
"30ml tomato paste",
|
||||
"25g all-purpose flour",
|
||||
"4 fresh thyme sprigs",
|
||||
"2 bay leaves",
|
||||
"45ml olive oil",
|
||||
"Salt and black pepper to taste"
|
||||
],
|
||||
"instructions": [
|
||||
"Toss the beef in the wine with thyme and bay leaves. Let it marinate overnight or at least 2 hours in the fridge. Drain and pat completely dry before cooking.",
|
||||
"In a large Dutch oven over medium heat, cook the bacon lardons until golden and crispy. Remove and set aside, leaving the bacon fat in the pot.",
|
||||
"Add the olive oil to the pot. Working in batches, sear the beef on all sides until deeply browned. Do not crowd the pan. Season with salt and pepper. Set beef aside.",
|
||||
"In the same pot, sauté the pearl onions and carrots for 5 minutes. Add the garlic and tomato paste, cook for 1 minute. Sprinkle in the flour and stir to coat everything.",
|
||||
"Pour in the wine and beef stock, scraping all the browned bits off the bottom. Return the beef and bacon to the pot. Tuck in the thyme and bay leaves. Bring to a simmer, then cover and transfer to a 160°C oven (fan: 140°C) for 3 hours.",
|
||||
"About 30 minutes before the braise is done, sauté the mushrooms in a separate pan with a little butter and salt until golden. Add them to the stew for the last 20 minutes.",
|
||||
"Remove bay leaves and thyme sprigs. Taste and adjust seasoning. Serve over mashed potatoes, egg noodles, or with crusty bread."
|
||||
],
|
||||
"id": "beef-bourguignon-housewife-flex",
|
||||
"updatedAt": "2026-03-12T17:11:05.472Z"
|
||||
}
|
||||
29
recipes/bubble-and-squeak.json
Normal file
29
recipes/bubble-and-squeak.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"title": "Bubble & Squeak",
|
||||
"tags": [
|
||||
"Breakfast",
|
||||
"Lunch"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "20 mins",
|
||||
"description": "Monday morning in Victorian Britain meant turning Sunday's leftover potatoes and cabbage into something crispy, golden, and deeply satisfying. Zero waste, maximum flavor, genuinely iconic energy.",
|
||||
"ingredients": [
|
||||
"500g leftover mashed or boiled potatoes",
|
||||
"300g leftover cooked cabbage or Brussels sprouts",
|
||||
"1 yellow onion, finely diced",
|
||||
"30g butter",
|
||||
"1 tablespoon olive oil",
|
||||
"1 teaspoon Worcestershire sauce",
|
||||
"Salt and black pepper to taste"
|
||||
],
|
||||
"instructions": [
|
||||
"In a large bowl, roughly mash together potatoes and cabbage. Don't over-mix — you want texture, not a smooth paste. Season with salt, pepper, and Worcestershire sauce.",
|
||||
"Melt butter with olive oil in a large non-stick frying pan over medium heat. Add onion and cook for 5-6 minutes until soft and golden.",
|
||||
"Add the potato and cabbage mixture to the pan. Stir to combine with the onion, then press the whole thing down firmly into an even patty covering the base of the pan.",
|
||||
"Cook without touching for 10-12 minutes over medium heat until a deep golden crust forms on the bottom.",
|
||||
"Either flip the whole cake for a crispy top and bottom, or fold it in half omelette-style. Slide onto a plate and serve immediately."
|
||||
],
|
||||
"id": "bubble-and-squeak",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
31
recipes/chocolate-lava-cake.json
Normal file
31
recipes/chocolate-lava-cake.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "Chocolate Lava Cake",
|
||||
"tags": [
|
||||
"Dessert"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "15 mins",
|
||||
"cookTime": "12 mins",
|
||||
"description": "A molten chocolate centre that flows like actual lava when you cut into it. Dramatic, impressive, and takes exactly 12 minutes to bake. The dinner party flex of all time.",
|
||||
"ingredients": [
|
||||
"150g dark chocolate (70%), chopped",
|
||||
"100g butter",
|
||||
"2 eggs",
|
||||
"2 egg yolks",
|
||||
"80g caster sugar",
|
||||
"30g plain flour",
|
||||
"1 tablespoon cocoa powder, for dusting",
|
||||
"1 teaspoon vanilla extract",
|
||||
"1 pinch salt"
|
||||
],
|
||||
"instructions": [
|
||||
"Butter 4 ramekins generously and dust with cocoa powder, tapping out any excess. Pop them in the fridge while you make the batter.",
|
||||
"Melt dark chocolate and butter together in a heatproof bowl over barely simmering water, stirring until smooth. Remove from heat and let cool slightly.",
|
||||
"In a separate bowl whisk eggs, egg yolks, and caster sugar together until pale, thick, and ribbony — about 3-4 minutes. Add vanilla extract.",
|
||||
"Pour the chocolate mixture into the egg mixture and fold gently. Sift in flour and salt and fold until just combined — do not overmix.",
|
||||
"Divide batter evenly between the prepared ramekins. Refrigerate for up to 24 hours if prepping ahead.",
|
||||
"Preheat oven to 200°C / 400°F. Bake for 10-12 minutes until the edges are set but the centre still has a wobble. Run a knife around the edge, place a plate on top, flip, and serve IMMEDIATELY."
|
||||
],
|
||||
"id": "chocolate-lava-cake",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
33
recipes/classic-banana-bread.json
Normal file
33
recipes/classic-banana-bread.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"title": "Classic Banana Bread",
|
||||
"tags": [
|
||||
"Breakfast",
|
||||
"Snack"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "60 mins",
|
||||
"description": "The loaf that started a thousand arguments about whose recipe is better. Moist, dense, naturally sweet, and the best use of those sad black bananas sitting on your counter right now.",
|
||||
"ingredients": [
|
||||
"3 very ripe bananas",
|
||||
"200g plain flour",
|
||||
"80g butter, melted",
|
||||
"150g brown sugar",
|
||||
"2 eggs",
|
||||
"1 teaspoon vanilla extract",
|
||||
"1 teaspoon baking soda",
|
||||
"1 teaspoon cinnamon",
|
||||
"1 pinch salt",
|
||||
"60g walnuts or chocolate chips (optional)"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 175°C / 350°F. Grease a standard loaf tin and line with parchment paper.",
|
||||
"Peel bananas and mash in a large bowl until completely smooth.",
|
||||
"Whisk in melted butter, brown sugar, eggs, and vanilla extract until combined.",
|
||||
"Sprinkle in flour, baking soda, cinnamon, and salt. Fold gently until just combined — stop the moment you can't see dry flour. Fold in walnuts or chocolate chips if using.",
|
||||
"Pour into the prepared loaf tin and bake for 55-65 minutes until a skewer inserted in the centre comes out clean.",
|
||||
"Cool in the tin for 10 minutes, then turn out onto a wire rack. Slice only when fully cool."
|
||||
],
|
||||
"id": "classic-banana-bread",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
36
recipes/classic-beef-veggie-chili.json
Normal file
36
recipes/classic-beef-veggie-chili.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"title": "Classic Beef & Veggie Chili",
|
||||
"tags": [
|
||||
"Lunch",
|
||||
"Dinner"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "35 mins",
|
||||
"description": "A hearty, no-fuss chili that freezes like a dream and reheats even better than day one. She's low maintenance, high reward.",
|
||||
"ingredients": [
|
||||
"500g ground beef (80/20)",
|
||||
"400g canned crushed tomatoes",
|
||||
"400g canned kidney beans, drained",
|
||||
"1 yellow onion, diced",
|
||||
"1 red bell pepper, diced",
|
||||
"3 garlic cloves, minced",
|
||||
"240ml beef broth",
|
||||
"2 tablespoons chili powder",
|
||||
"1 teaspoon cumin",
|
||||
"1 teaspoon smoked paprika",
|
||||
"Salt and black pepper to taste",
|
||||
"1 tablespoon olive oil"
|
||||
],
|
||||
"instructions": [
|
||||
"Heat olive oil in a large pot over medium heat. Add onion and red bell pepper, cook until softened. Add garlic and cook for another minute.",
|
||||
"Add ground beef to the pot. Break it up and cook until no pink remains, about 8 minutes. Drain excess fat if needed.",
|
||||
"Stir in chili powder, cumin, smoked paprika, salt, and pepper. Cook for 1 minute to bloom the spices.",
|
||||
"Pour in crushed tomatoes, kidney beans, and beef broth. Stir to combine and bring to a boil.",
|
||||
"Reduce heat to low and simmer uncovered for at least 20 minutes, stirring occasionally, until thick and rich.",
|
||||
"Let cool completely before portioning into freezer-safe containers or zip-lock bags. Freeze flat to save space. Keeps for up to 3 months.",
|
||||
"To serve, reheat on the stove with a splash of broth. Top with sour cream, cheddar, or green onions."
|
||||
],
|
||||
"id": "classic-beef-veggie-chili",
|
||||
"updatedAt": "2026-03-12T17:11:52.268Z"
|
||||
}
|
||||
40
recipes/classic-freezer-lasagna.json
Normal file
40
recipes/classic-freezer-lasagna.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"title": "Classic Freezer Lasagna",
|
||||
"tags": [
|
||||
"Lunch",
|
||||
"Dinner"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "20 mins",
|
||||
"cookTime": "45 mins",
|
||||
"description": "The undisputed queen of freezer meals. Layers of rich meat sauce, creamy béchamel, and bubbling cheese. She's dramatic, she's delicious, she's worth every single dish.",
|
||||
"ingredients": [
|
||||
"200g lasagna sheets (dried)",
|
||||
"400g ground beef",
|
||||
"400g canned crushed tomatoes",
|
||||
"2 tablespoons tomato paste",
|
||||
"1 yellow onion, diced",
|
||||
"3 garlic cloves, minced",
|
||||
"1 teaspoon dried oregano",
|
||||
"1 teaspoon dried basil",
|
||||
"40g butter",
|
||||
"40g plain flour",
|
||||
"500ml whole milk",
|
||||
"1 pinch nutmeg, grated",
|
||||
"200g mozzarella, shredded",
|
||||
"60g Parmesan, freshly grated",
|
||||
"1 tablespoon olive oil",
|
||||
"Salt and black pepper to taste"
|
||||
],
|
||||
"instructions": [
|
||||
"Heat olive oil in a pan over medium heat. Sauté onion until soft, then add garlic for 1 minute. Add ground beef and brown completely. Stir in tomato paste, crushed tomatoes, oregano, basil, salt and pepper. Simmer for 20 minutes until thick and deeply flavored.",
|
||||
"Melt butter in a saucepan over medium heat. Whisk in flour and cook for 1 minute. Gradually pour in milk, whisking constantly to avoid lumps. Stir until thick and smooth, then season with salt, pepper, and nutmeg.",
|
||||
"Preheat oven to 190°C / 375°F. Lightly grease your baking dish. If using dried lasagna sheets that require pre-boiling, cook them now until just under al dente.",
|
||||
"Spread a thin layer of meat sauce on the bottom of the dish. Add a layer of lasagna sheets, then béchamel, then meat sauce, then a sprinkle of mozzarella. Repeat until you run out, finishing with béchamel on top. Crown with mozzarella and Parmesan.",
|
||||
"Cover with foil and bake for 30 minutes. Remove foil and bake another 15 minutes until golden and bubbling on top.",
|
||||
"Let rest for 15 minutes before slicing — non-negotiable, she needs it.",
|
||||
"To freeze: cool completely, wrap tightly in cling film and foil. Keeps up to 3 months. Thaw overnight in the fridge, then reheat covered at 180°C for 30-40 mins."
|
||||
],
|
||||
"id": "classic-freezer-lasagna",
|
||||
"updatedAt": "2026-03-12T17:12:20.421Z"
|
||||
}
|
||||
31
recipes/classic-roast-chicken.json
Normal file
31
recipes/classic-roast-chicken.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "Classic Roast Chicken",
|
||||
"tags": [
|
||||
"Dinner"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "15 mins",
|
||||
"cookTime": "1 hr 20 mins",
|
||||
"description": "The crown jewel of Sunday dinners. Crispy golden skin, juicy meat, and drippings that make the whole house smell incredible. Master her and you master everything.",
|
||||
"ingredients": [
|
||||
"1 whole chicken (approx 1.5kg)",
|
||||
"60g butter, softened",
|
||||
"4 garlic cloves, minced",
|
||||
"4 sprigs fresh thyme",
|
||||
"2 sprigs fresh rosemary",
|
||||
"1 lemon",
|
||||
"2 tablespoons olive oil",
|
||||
"Salt and black pepper to taste"
|
||||
],
|
||||
"instructions": [
|
||||
"Take chicken out of the fridge 30 minutes before cooking. Preheat oven to 200°C / 400°F. Pat the chicken completely dry with paper towels.",
|
||||
"Mix softened butter with garlic, thyme and rosemary leaves, lemon zest, salt and pepper.",
|
||||
"Gently loosen the skin over the breast and push butter rub underneath. Rub remaining butter all over the outside. Drizzle with olive oil and season generously.",
|
||||
"Stuff the cavity with the halved lemon and remaining herb sprigs. Tie the legs together with kitchen twine.",
|
||||
"Place breast-side up in a roasting tin. Roast for 20 minutes per 500g plus an extra 20 minutes, basting with pan juices halfway through.",
|
||||
"Pierce the thickest part of the thigh — juices should run completely clear and internal temperature should reach 75°C / 165°F.",
|
||||
"Cover loosely with foil and rest for 15-20 minutes before carving."
|
||||
],
|
||||
"id": "classic-roast-chicken",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
31
recipes/crispy-veggie-fritters.json
Normal file
31
recipes/crispy-veggie-fritters.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "Crispy Veggie Fritters",
|
||||
"tags": [
|
||||
"Snack",
|
||||
"Lunch"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "20 mins",
|
||||
"description": "Sneaky vegetables disguised as crispy golden fritters. Kids have absolutely no idea what's happening and that's exactly the point. Great warm or cold, brilliant for lunchboxes.",
|
||||
"ingredients": [
|
||||
"2 zucchinis, grated",
|
||||
"150g corn kernels (fresh or canned)",
|
||||
"80g shredded cheddar cheese",
|
||||
"80g plain flour",
|
||||
"2 eggs",
|
||||
"½ teaspoon garlic powder",
|
||||
"Salt and black pepper to taste",
|
||||
"2 tablespoons olive oil for frying"
|
||||
],
|
||||
"instructions": [
|
||||
"Grate zucchini and place in a clean tea towel. Squeeze out as much moisture as possible — this is the most important step. Wet zucchini means soggy fritters.",
|
||||
"In a large bowl combine squeezed zucchini, corn, cheddar, flour, eggs, garlic powder, salt and pepper. Mix until a thick batter forms. Add a little more flour if too wet.",
|
||||
"Heat olive oil in a large non-stick pan over medium heat. Drop heaped tablespoons of batter into the pan and flatten slightly.",
|
||||
"Cook for 3-4 minutes per side until deeply golden and cooked through. Work in batches, adding more oil as needed.",
|
||||
"Transfer to a plate lined with paper towel to drain.",
|
||||
"Store in the fridge in an airtight container for up to 4 days, or freeze for up to 2 months. Reheat in the oven at 180°C for 10 minutes to bring back the crispiness."
|
||||
],
|
||||
"id": "crispy-veggie-fritters",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
30
recipes/egg-muffins.json
Normal file
30
recipes/egg-muffins.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"title": "Egg Muffins",
|
||||
"tags": [
|
||||
"Breakfast"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "20 mins",
|
||||
"description": "Mini frittatas baked in a muffin tin. Endlessly customizable, meal-preppable, and ready to grab on your way out the door. She's efficient and she knows it.",
|
||||
"ingredients": [
|
||||
"6 large eggs",
|
||||
"60ml whole milk",
|
||||
"80g shredded cheddar cheese",
|
||||
"60g diced bell pepper (any color)",
|
||||
"60g baby spinach, roughly chopped",
|
||||
"40g diced cooked bacon or ham",
|
||||
"Salt and black pepper to taste",
|
||||
"Olive oil or cooking spray for greasing"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 180°C / 350°F. Grease a 12-cup muffin tin well with olive oil or cooking spray.",
|
||||
"Whisk together eggs and milk in a large bowl. Season with salt and pepper.",
|
||||
"Stir in cheese, bell pepper, spinach, and bacon or ham.",
|
||||
"Pour the mixture evenly into the muffin cups, filling each about three-quarters full.",
|
||||
"Bake for 18-20 minutes until the eggs are set and lightly golden on top.",
|
||||
"Let cool for 5 minutes before removing from the tin. Store in an airtight container in the fridge for up to 5 days. Reheat in the microwave for 30-45 seconds."
|
||||
],
|
||||
"id": "egg-muffins",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
25
recipes/homemade-fruit-leather.json
Normal file
25
recipes/homemade-fruit-leather.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"title": "Homemade Fruit Leather",
|
||||
"tags": [
|
||||
"Snack"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "3 hrs",
|
||||
"description": "Basically a fruit roll-up but homemade and actually made of fruit. Kids go absolutely feral for these and you get to feel smug about it. Low effort, high reward, very lunchbox-coded.",
|
||||
"ingredients": [
|
||||
"500g fresh or frozen strawberries",
|
||||
"2 tablespoons honey or maple syrup",
|
||||
"1 tablespoon lemon juice"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 80°C / 175°F. Line a large baking sheet with parchment paper or a silicone mat — do NOT use foil, it will stick.",
|
||||
"Add strawberries, honey or maple syrup, and lemon juice to a blender. Blitz until completely smooth. Taste and adjust sweetness if needed.",
|
||||
"Pour the puree onto the lined baking sheet and spread into a thin, even layer about 3-4mm thick using an offset spatula.",
|
||||
"Bake for 2.5 to 3 hours until the surface is no longer sticky to the touch and peels away from the parchment cleanly.",
|
||||
"Let cool completely, then peel off the parchment. Cut into strips with scissors or a pizza cutter.",
|
||||
"Roll each strip up in a fresh piece of parchment and twist the ends. Store in an airtight container at room temperature for up to 2 weeks."
|
||||
],
|
||||
"id": "homemade-fruit-leather",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
31
recipes/homemade-granola.json
Normal file
31
recipes/homemade-granola.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "Homemade Granola",
|
||||
"tags": [
|
||||
"Breakfast"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "25 mins",
|
||||
"description": "Make one big batch and she lasts two weeks on the counter. Pairs with yogurt, milk, or just eaten straight from the jar at midnight. No judgment here.",
|
||||
"ingredients": [
|
||||
"300g rolled oats",
|
||||
"80g mixed nuts (almonds, pecans, walnuts), roughly chopped",
|
||||
"60ml honey or maple syrup",
|
||||
"60ml coconut oil, melted",
|
||||
"1 teaspoon vanilla extract",
|
||||
"1 teaspoon cinnamon",
|
||||
"1 pinch salt",
|
||||
"80g dried fruit (raisins, cranberries, or apricots), added after baking"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 160°C / 325°F. Line a large baking sheet with parchment paper.",
|
||||
"In a large bowl, combine rolled oats, chopped nuts, cinnamon, and salt.",
|
||||
"Pour in melted coconut oil, honey or maple syrup, and vanilla extract. Stir until everything is evenly coated.",
|
||||
"Spread the mixture in a thin, even layer on the prepared baking sheet.",
|
||||
"Bake for 20-25 minutes, stirring once halfway through, until golden and fragrant. Watch her closely near the end — she burns fast.",
|
||||
"Remove from oven and let cool completely on the tray without stirring — this is how she gets those satisfying clusters.",
|
||||
"Once fully cool, stir in the dried fruit. Store in an airtight jar at room temperature for up to 2 weeks."
|
||||
],
|
||||
"id": "homemade-granola",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
29
recipes/overnight-oats.json
Normal file
29
recipes/overnight-oats.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"title": "Overnight Oats",
|
||||
"tags": [
|
||||
"Breakfast"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "0 mins",
|
||||
"description": "Zero cooking, just vibes and a mason jar. Prep four jars on Sunday and breakfast is completely handled for most of the week. Truly unhinged how easy this is.",
|
||||
"ingredients": [
|
||||
"400g rolled oats",
|
||||
"480ml whole milk (or oat milk)",
|
||||
"240g Greek yogurt",
|
||||
"4 tablespoons chia seeds",
|
||||
"4 tablespoons honey or maple syrup",
|
||||
"2 teaspoons vanilla extract",
|
||||
"Fresh fruit, nut butter, or granola to top"
|
||||
],
|
||||
"instructions": [
|
||||
"Divide rolled oats evenly between 4 mason jars or airtight containers.",
|
||||
"Add chia seeds, honey or maple syrup, and vanilla extract to each jar.",
|
||||
"Pour milk evenly across the jars and add a dollop of Greek yogurt to each.",
|
||||
"Stir each jar well until fully combined.",
|
||||
"Seal and refrigerate overnight or for at least 6 hours.",
|
||||
"In the morning, give it a stir and add your toppings of choice — fruit, nut butter, granola, whatever she needs. Keeps in the fridge for up to 5 days."
|
||||
],
|
||||
"id": "overnight-oats",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
34
recipes/pea-and-ham-soup.json
Normal file
34
recipes/pea-and-ham-soup.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Pea & Ham Soup",
|
||||
"tags": [
|
||||
"Dinner",
|
||||
"Lunch"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "15 mins",
|
||||
"cookTime": "2 hrs",
|
||||
"description": "The ultimate nothing-goes-to-waste soup. A ham bone simmered low and slow with split peas until thick, silky, and deeply savory. Victorian frugality at its absolute finest and most delicious.",
|
||||
"ingredients": [
|
||||
"400g dried split green peas, soaked overnight",
|
||||
"1 ham hock or leftover ham bone",
|
||||
"1 yellow onion, diced",
|
||||
"2 carrots, diced",
|
||||
"2 celery stalks, diced",
|
||||
"3 garlic cloves, minced",
|
||||
"2 bay leaves",
|
||||
"3 sprigs fresh thyme",
|
||||
"1500ml chicken or vegetable stock",
|
||||
"1 tablespoon butter",
|
||||
"Salt and black pepper to taste"
|
||||
],
|
||||
"instructions": [
|
||||
"Rinse split peas and soak in cold water overnight or for at least 2 hours. Drain before using.",
|
||||
"Melt butter in a large heavy pot over medium heat. Add onion, carrots, and celery and cook for 8 minutes until soft. Add garlic and cook 1 more minute.",
|
||||
"Add drained peas, ham hock, bay leaves, thyme, and stock. Bring to a boil, then skim off any foam that rises to the top.",
|
||||
"Reduce heat to low. Cover and simmer for 1.5 to 2 hours until the peas have completely broken down and the soup is thick and velvety, stirring occasionally.",
|
||||
"Remove the ham hock. Pull the meat off the bone, shred it, and stir it back into the soup. Discard the bone, bay leaves, and thyme.",
|
||||
"Season with salt and pepper — taste first as the ham can be salty already. Serve with crusty bread."
|
||||
],
|
||||
"id": "pea-and-ham-soup",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"id": "sample-lemon-pasta",
|
||||
"title": "Creamy Lemon Pasta",
|
||||
"category": "Dinner",
|
||||
"description": "A bright, zesty pasta that comes together in under 30 minutes. Perfect for busy weeknights when you want something that feels fancy but isn't fussy.",
|
||||
"tags": [
|
||||
"Lunch",
|
||||
"Dinner"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "15 mins",
|
||||
"description": "A bright, zesty pasta that comes together in under 30 minutes. Perfect for busy weeknights when you want something that feels fancy but isn't fussy.",
|
||||
"ingredients": [
|
||||
"400g spaghetti or linguine",
|
||||
"3 tablespoons butter",
|
||||
@@ -28,6 +30,6 @@
|
||||
"Remove from heat, add remaining Parmesan, and season with salt and pepper.",
|
||||
"Serve immediately, garnished with fresh herbs and red pepper flakes if desired."
|
||||
],
|
||||
"createdAt": "2025-03-12T10:00:00.000Z",
|
||||
"updatedAt": "2025-03-12T10:00:00.000Z"
|
||||
"id": "sample-lemon-pasta",
|
||||
"updatedAt": "2026-03-12T17:07:22.477Z"
|
||||
}
|
||||
38
recipes/slow-cooked-bolognese.json
Normal file
38
recipes/slow-cooked-bolognese.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"title": "Slow Cooked Bolognese",
|
||||
"tags": [
|
||||
"Dinner"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "15 mins",
|
||||
"cookTime": "2 hrs 30 mins",
|
||||
"description": "Not the quick weeknight kind — the REAL bolognese. Slow, rich, deeply savory, built on patience and love. Tastes like it came from a Nonna in Bologna and that's exactly the point.",
|
||||
"ingredients": [
|
||||
"500g ground beef (or half beef half pork)",
|
||||
"1 yellow onion, finely diced",
|
||||
"1 carrot, finely diced",
|
||||
"1 celery stalk, finely diced",
|
||||
"3 garlic cloves, minced",
|
||||
"2 tablespoons tomato paste",
|
||||
"400g canned crushed tomatoes",
|
||||
"120ml dry white or red wine",
|
||||
"120ml whole milk",
|
||||
"240ml beef stock",
|
||||
"2 tablespoons olive oil",
|
||||
"1 teaspoon dried oregano",
|
||||
"2 bay leaves",
|
||||
"Salt and black pepper to taste",
|
||||
"60g Parmesan, to serve"
|
||||
],
|
||||
"instructions": [
|
||||
"Heat olive oil in a heavy-bottomed pot over medium-low heat. Add onion, carrot, and celery. Cook slowly for 10-12 minutes until completely softened. Add garlic and cook 1 more minute.",
|
||||
"Turn heat to medium-high. Add ground beef and break it up. Cook until all moisture evaporates and the meat browns — about 10 minutes.",
|
||||
"Pour in wine and scrape up any browned bits. Cook until fully absorbed, about 3-4 minutes.",
|
||||
"Stir in tomato paste and cook for 2 minutes. Add crushed tomatoes, beef stock, oregano, bay leaves, salt and pepper. Bring to a gentle simmer.",
|
||||
"Reduce heat to the lowest setting. Simmer uncovered for at least 2 hours, stirring occasionally, adding splashes of stock if it gets too dry.",
|
||||
"In the last 20 minutes, stir in the whole milk. Let it absorb fully.",
|
||||
"Remove bay leaves. Serve over tagliatelle or pappardelle, finished with Parmesan."
|
||||
],
|
||||
"id": "slow-cooked-bolognese",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
34
recipes/victoria-sponge.json
Normal file
34
recipes/victoria-sponge.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Victoria Sponge",
|
||||
"tags": [
|
||||
"Dessert",
|
||||
"Snack"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "20 mins",
|
||||
"cookTime": "25 mins",
|
||||
"description": "The quintessential British celebration cake. Two golden sponges sandwiched with jam and cream, dusted with icing sugar. Simple, elegant, timeless. She doesn't need decorating — she IS the decoration.",
|
||||
"ingredients": [
|
||||
"200g butter, softened",
|
||||
"200g caster sugar",
|
||||
"4 eggs",
|
||||
"200g self-raising flour",
|
||||
"1 teaspoon vanilla extract",
|
||||
"1 teaspoon baking powder",
|
||||
"2 tablespoons milk",
|
||||
"4 tablespoons strawberry jam",
|
||||
"200ml double cream",
|
||||
"1 tablespoon icing sugar for dusting"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 180°C / 350°F. Grease two 20cm round cake tins and line the bases with parchment paper.",
|
||||
"Beat butter and caster sugar together with an electric mixer for 4-5 minutes until very pale, light, and fluffy.",
|
||||
"Add eggs one at a time, beating well after each. Add vanilla extract. If the mixture curdles, add a spoonful of flour.",
|
||||
"Sift in self-raising flour and baking powder. Fold gently until just combined. Add milk to loosen to a dropping consistency.",
|
||||
"Divide batter evenly between the two tins and smooth the tops. Bake for 20-25 minutes until golden and springy to the touch.",
|
||||
"Turn out onto a wire rack and cool completely before filling.",
|
||||
"Whip double cream to soft peaks. Spread jam on the bottom layer, top with whipped cream, place second sponge on top and dust with icing sugar. Serve the same day."
|
||||
],
|
||||
"id": "victoria-sponge",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
27
recipes/victorian-lemon-curd.json
Normal file
27
recipes/victorian-lemon-curd.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"title": "Victorian Lemon Curd",
|
||||
"tags": [
|
||||
"Breakfast",
|
||||
"Snack"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "10 mins",
|
||||
"cookTime": "15 mins",
|
||||
"description": "The Victorian invention that changed breakfast forever. Silky, tangy, intensely lemony, and spreadable on absolutely everything. She's been around since the 1800s and she's not going anywhere.",
|
||||
"ingredients": [
|
||||
"3 lemons, zested and juiced (approx 100ml juice)",
|
||||
"200g caster sugar",
|
||||
"3 eggs",
|
||||
"2 egg yolks",
|
||||
"100g butter, cubed"
|
||||
],
|
||||
"instructions": [
|
||||
"Zest all three lemons then juice them. You need about 100ml of juice. Fish out any pips.",
|
||||
"Whisk together caster sugar, eggs, egg yolks, lemon zest and juice in a heatproof bowl until smooth.",
|
||||
"Place the bowl over a pan of barely simmering water — the bowl must not touch the water. Stir constantly for 10-12 minutes until thick enough to coat the back of a spoon.",
|
||||
"Remove from heat. Add butter a few cubes at a time, stirring after each addition until fully melted and glossy.",
|
||||
"Pour into sterilized jars while still warm. Cool to room temperature, then seal and refrigerate. Keeps for up to 3 weeks."
|
||||
],
|
||||
"id": "victorian-lemon-curd",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
33
recipes/victorian-seed-cake.json
Normal file
33
recipes/victorian-seed-cake.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"title": "Victorian Seed Cake",
|
||||
"tags": [
|
||||
"Breakfast",
|
||||
"Snack"
|
||||
],
|
||||
"servings": "4",
|
||||
"prepTime": "15 mins",
|
||||
"cookTime": "60 mins",
|
||||
"description": "The quintessential Victorian afternoon tea cake. Caraway seeds give her a subtle anise-like warmth that sounds weird and tastes absolutely elegant. She was on every respectable Victorian table and deserves a serious comeback.",
|
||||
"ingredients": [
|
||||
"175g butter, softened",
|
||||
"175g caster sugar",
|
||||
"3 eggs",
|
||||
"225g self-raising flour",
|
||||
"2 teaspoons caraway seeds",
|
||||
"3 tablespoons milk",
|
||||
"1 teaspoon vanilla extract",
|
||||
"Zest of 1 lemon",
|
||||
"1 tablespoon demerara sugar for topping"
|
||||
],
|
||||
"instructions": [
|
||||
"Preheat oven to 170°C / 340°F. Grease a 900g loaf tin and line with parchment paper.",
|
||||
"Beat butter and caster sugar together for 4-5 minutes until very pale and fluffy. Add vanilla extract and lemon zest and beat briefly.",
|
||||
"Add eggs one at a time, beating well after each. If it looks like it's splitting, add a spoonful of flour.",
|
||||
"Sift in self-raising flour and fold gently. Add caraway seeds and milk and fold until just combined.",
|
||||
"Spoon into the prepared tin and smooth the top. Sprinkle generously with demerara sugar.",
|
||||
"Bake for 55-65 minutes until a skewer comes out clean and the top is golden with a crunchy crust.",
|
||||
"Cool in the tin for 10 minutes then turn out onto a wire rack. Serve slightly warm, sliced, with butter and a pot of tea."
|
||||
],
|
||||
"id": "victorian-seed-cake",
|
||||
"updatedAt": "2026-03-12T10:00:00.000Z"
|
||||
}
|
||||
Reference in New Issue
Block a user