diff options
| author | Ken D'Ambrosio <ken@claude> | 2026-05-25 00:46:10 +0000 |
|---|---|---|
| committer | Ken D'Ambrosio <ken@claude> | 2026-05-25 00:46:10 +0000 |
| commit | 55bcec90c14db6f2956ed51cf4df1503c0767f81 (patch) | |
| tree | f25bfb8c46366b5d3dc6b4f66e242c65094b4ada /app/templates/recipe_add.html | |
Initial commit — menu.jots.org Flask/SQLite meal planner
Full-featured weekly menu planner with:
- Recipe library with ratings, comments, cuisine/nationality, added-by attribution
- AI recipe assistant (Claude) with URL fetching and file upload
- Weekly meal plan grid with shopping list generation
- Sort by name, prep/cook time, or rating
- Print and copy-link support
- Deployed on LXC container (192.168.10.51) behind Apache reverse proxy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/templates/recipe_add.html')
| -rw-r--r-- | app/templates/recipe_add.html | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/app/templates/recipe_add.html b/app/templates/recipe_add.html new file mode 100644 index 0000000..f0a0538 --- /dev/null +++ b/app/templates/recipe_add.html @@ -0,0 +1,216 @@ +{% extends "base.html" %} +{% block title %}Add Recipe — Menu Planner{% endblock %} + +{% block content %} +<div class="mb-3"> + <a href="/recipes" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Back to Recipes</a> +</div> + +<h1 class="h3 fw-bold mb-4">Add a New Recipe</h1> + +<form method="POST" action="/recipes/add" id="recipeForm"> + <div class="row g-4"> + + <!-- Left column: basic info + instructions --> + <div class="col-lg-7"> + + <!-- Basic info --> + <div class="card shadow-sm mb-4"> + <div class="card-header fw-semibold">Basic Information</div> + <div class="card-body"> + <div class="mb-3"> + <label class="form-label fw-semibold">Recipe Name <span class="text-danger">*</span></label> + <input type="text" name="name" class="form-control" required + value="{{ form.get('name','') }}" placeholder="e.g. Chicken Marsala"> + </div> + <div class="row g-3 mb-3"> + <div class="col-sm-6"> + <label class="form-label fw-semibold">Cuisine <span class="text-danger">*</span></label> + <input type="text" name="cuisine" class="form-control" required + list="cuisine-list" autocomplete="off" + placeholder="e.g. Italian, Mexican…" + value="{{ form.get('cuisine', '') }}"> + <datalist id="cuisine-list"> + {% for c in cuisines %}<option value="{{ c }}">{% endfor %} + </datalist> + </div> + <div class="col-sm-6"> + <label class="form-label fw-semibold">Default Servings</label> + <input type="number" name="servings" class="form-control" min="1" max="20" + value="{{ form.get('servings', 2) }}"> + <div class="form-text">Base recipe yields (shopping list scales from this)</div> + </div> + </div> + <div class="mb-0"> + <label class="form-label fw-semibold">Description</label> + <textarea name="description" class="form-control" rows="2" + placeholder="One sentence that makes this dish sound irresistible…">{{ form.get('description','') }}</textarea> + </div> + </div> + </div> + + <!-- Timing --> + <div class="card shadow-sm mb-4"> + <div class="card-header fw-semibold">Timing</div> + <div class="card-body"> + <div class="row g-3"> + <div class="col-sm-6"> + <label class="form-label fw-semibold">Prep Time (minutes)</label> + <input type="number" name="prep_time" class="form-control" min="0" + value="{{ form.get('prep_time', '') }}" placeholder="15"> + </div> + <div class="col-sm-6"> + <label class="form-label fw-semibold">Cook Time (minutes)</label> + <input type="number" name="cook_time" class="form-control" min="0" + value="{{ form.get('cook_time', '') }}" placeholder="30"> + </div> + </div> + </div> + </div> + + <!-- Instructions --> + <div class="card shadow-sm mb-4"> + <div class="card-header fw-semibold">Instructions</div> + <div class="card-body"> + <textarea name="instructions" class="form-control font-monospace" rows="10" + placeholder="1. First step. 2. Second step. 3. Third step.">{{ form.get('instructions','') }}</textarea> + <div class="form-text mt-1">Number each step: <code>1. Heat oil…</code></div> + </div> + </div> + + </div> + + <!-- Right column: nutrition + ingredients --> + <div class="col-lg-5"> + + <!-- Nutrition --> + <div class="card shadow-sm mb-4"> + <div class="card-header fw-semibold">Nutrition <span class="text-muted small fw-normal">(per serving)</span></div> + <div class="card-body"> + <div class="row g-2"> + <div class="col-6"> + <label class="form-label small fw-semibold">Calories</label> + <div class="input-group input-group-sm"> + <input type="number" name="calories_per_serving" class="form-control" + min="0" step="5" value="{{ form.get('calories_per_serving','') }}" placeholder="400"> + <span class="input-group-text">kcal</span> + </div> + </div> + <div class="col-6"> + <label class="form-label small fw-semibold">Net Carbs</label> + <div class="input-group input-group-sm"> + <input type="number" name="carbs_per_serving" class="form-control" + min="0" step="0.5" value="{{ form.get('carbs_per_serving','') }}" placeholder="8"> + <span class="input-group-text">g</span> + </div> + </div> + <div class="col-6"> + <label class="form-label small fw-semibold">Protein</label> + <div class="input-group input-group-sm"> + <input type="number" name="protein_per_serving" class="form-control" + min="0" step="0.5" value="{{ form.get('protein_per_serving','') }}" placeholder="40"> + <span class="input-group-text">g</span> + </div> + </div> + <div class="col-6"> + <label class="form-label small fw-semibold">Fat</label> + <div class="input-group input-group-sm"> + <input type="number" name="fat_per_serving" class="form-control" + min="0" step="0.5" value="{{ form.get('fat_per_serving','') }}" placeholder="20"> + <span class="input-group-text">g</span> + </div> + </div> + </div> + </div> + </div> + + <!-- Ingredients --> + <div class="card shadow-sm"> + <div class="card-header d-flex align-items-center justify-content-between fw-semibold"> + Ingredients + <button type="button" id="addIngBtn" class="btn btn-sm btn-outline-primary"> + <i class="bi bi-plus"></i> Add Row + </button> + </div> + <div class="card-body p-2"> + <table class="table table-sm mb-0" id="ingTable"> + <thead class="table-light"> + <tr> + <th style="width:70px">Qty</th> + <th style="width:85px">Unit</th> + <th>Ingredient</th> + <th style="width:120px">Category</th> + <th style="width:30px"></th> + </tr> + </thead> + <tbody id="ingBody"> + <!-- 3 starter rows --> + </tbody> + </table> + </div> + </div> + + </div> + </div> + + <div class="d-flex gap-2 mt-4 justify-content-end"> + <a href="/recipes" class="btn btn-outline-secondary">Cancel</a> + <button type="submit" class="btn btn-primary px-4"> + <i class="bi bi-check-circle me-1"></i>Save Recipe + </button> + </div> +</form> +{% endblock %} + +{% block scripts %} +<script> +const UNITS = {{ units | tojson }}; +const CATS = {{ categories | tojson }}; + +function unitOptions(selected='whole') { + return UNITS.map(u => `<option value="${u}"${u===selected?' selected':''}>${u}</option>`).join(''); +} +function catOptions(selected='Produce') { + return CATS.map(c => `<option value="${c}"${c===selected?' selected':''}>${c}</option>`).join(''); +} + +function newRow(qty='', unit='whole', name='', cat='Produce') { + const tr = document.createElement('tr'); + tr.className = 'ing-row'; + tr.innerHTML = ` + <td><input type="number" name="ing_qty" class="form-control form-control-sm" min="0" step="0.25" value="${qty}" placeholder="1"></td> + <td><select name="ing_unit" class="form-select form-select-sm">${unitOptions(unit)}</select></td> + <td><input type="text" name="ing_name" class="form-control form-control-sm" value="${name}" placeholder="Ingredient…"></td> + <td><select name="ing_category" class="form-select form-select-sm">${catOptions(cat)}</select></td> + <td><button type="button" class="btn btn-sm btn-link text-danger p-0 remove-row" title="Remove"><i class="bi bi-x-lg"></i></button></td> + `; + return tr; +} + +const body = document.getElementById('ingBody'); + +// Seed 4 empty rows +for (let i = 0; i < 4; i++) body.appendChild(newRow()); + +document.getElementById('addIngBtn').addEventListener('click', () => { + body.appendChild(newRow()); + body.lastElementChild.querySelector('input[name="ing_name"]').focus(); +}); + +body.addEventListener('click', e => { + const btn = e.target.closest('.remove-row'); + if (btn) { + const rows = body.querySelectorAll('.ing-row'); + if (rows.length > 1) btn.closest('tr').remove(); + } +}); + +// Don't submit empty ingredient rows +document.getElementById('recipeForm').addEventListener('submit', function() { + body.querySelectorAll('.ing-row').forEach(row => { + const name = row.querySelector('input[name="ing_name"]').value.trim(); + if (!name) row.querySelectorAll('input, select').forEach(el => el.disabled = true); + }); +}); +</script> +{% endblock %} |
