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/shopping_list.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/shopping_list.html')
| -rw-r--r-- | app/templates/shopping_list.html | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/app/templates/shopping_list.html b/app/templates/shopping_list.html new file mode 100644 index 0000000..e6cfaaf --- /dev/null +++ b/app/templates/shopping_list.html @@ -0,0 +1,167 @@ +{% extends "base.html" %} +{% block title %}Shopping List — Menu Planner{% endblock %} + +{% block content %} +<div class="d-flex align-items-center justify-content-between mb-4 no-print"> + <div> + <h1 class="h3 fw-bold mb-0">Shopping List</h1> + <p class="text-muted mb-0"> + Week of {{ week_start.strftime('%B %d, %Y') }} + {% if total %} · {{ checked }}/{{ total }} checked{% endif %} + </p> + </div> + <div class="d-flex gap-2 align-items-center"> + <a href="/shopping-list?week={{ prev_week }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-chevron-left"></i></a> + <a href="/shopping-list" class="btn btn-outline-secondary btn-sm">This Week</a> + <a href="/shopping-list?week={{ next_week }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-chevron-right"></i></a> + <button onclick="window.print()" class="btn btn-outline-secondary btn-sm ms-2"> + <i class="bi bi-printer me-1"></i>Print + </button> + {% if current_user.is_authenticated %} + <button id="regenerateBtn" class="btn btn-success btn-sm"> + <i class="bi bi-arrow-clockwise me-1"></i>Regenerate + </button> + {% if total %} + <button id="clearBtn" class="btn btn-outline-danger btn-sm"> + <i class="bi bi-trash me-1"></i>Clear + </button> + {% endif %} + {% endif %}{# end is_authenticated #} + </div> +</div> + +{% if categories %} + +<!-- Progress bar --> +{% if total %} +<div class="mb-4 no-print"> + <div class="d-flex justify-content-between small text-muted mb-1"> + <span>{{ checked }} of {{ total }} items collected</span> + <span>{{ (checked / total * 100)|int }}%</span> + </div> + <div class="progress" style="height:8px"> + <div class="progress-bar bg-success" style="width: {{ (checked / total * 100)|int }}%"></div> + </div> +</div> +{% endif %} + +<div class="row g-4"> + {% for cat_name, items in categories %} + <div class="col-md-6 col-xl-4"> + <div class="card shadow-sm h-100"> + <div class="card-header fw-semibold d-flex align-items-center gap-2"> + {% if cat_name == 'Meat & Poultry' %}<i class="bi bi-egg-fried text-warning"></i> + {% elif cat_name == 'Seafood' %}<i class="bi bi-water text-info"></i> + {% elif cat_name == 'Dairy & Eggs' %}<i class="bi bi-cup-straw text-warning"></i> + {% elif cat_name == 'Produce' %}<i class="bi bi-flower1 text-success"></i> + {% elif cat_name == 'Pantry' %}<i class="bi bi-archive text-secondary"></i> + {% elif cat_name == 'Spices & Herbs' %}<i class="bi bi-asterisk text-danger"></i> + {% else %}<i class="bi bi-box text-muted"></i>{% endif %} + {{ cat_name }} + <span class="badge bg-secondary ms-auto">{{ items|length }}</span> + </div> + <ul class="list-group list-group-flush"> + {% for item in items %} + <li class="list-group-item shopping-item {% if item.checked %}checked{% endif %}" data-id="{{ item.id }}"> + <div class="d-flex align-items-center gap-2"> + <input type="checkbox" class="form-check-input flex-shrink-0 shop-check" id="item{{ item.id }}" + {% if item.checked %}checked{% endif %}> + <label for="item{{ item.id }}" class="mb-0 flex-grow-1 check-label"> + <span class="ing-qty fw-semibold me-1"> + {% if item.quantity == item.quantity|int %}{{ item.quantity|int }}{% else %}{{ item.quantity }}{% endif %} + {{ item.unit }} + </span> + {{ item.ingredient_name }} + </label> + {% if item.recipe_sources %} + <span class="text-muted small d-none d-sm-inline source-hint" title="{{ item.recipe_sources }}"> + <i class="bi bi-book"></i> + </span> + {% endif %} + </div> + {% if item.recipe_sources %} + <div class="small text-muted mt-1 ps-4 source-text">{{ item.recipe_sources }}</div> + {% endif %} + </li> + {% endfor %} + </ul> + </div> + </div> + {% endfor %} +</div> + +{% else %} +<div class="text-center py-5"> + <i class="bi bi-cart-x fs-1 text-muted"></i> + <p class="text-muted mt-3">No shopping list yet for this week.</p> + <p class="text-muted small">Add meals to your <a href="/meal-plan">meal plan</a> first, then click Regenerate.</p> + <button id="regenerateBtn2" class="btn btn-success mt-2"> + <i class="bi bi-arrow-clockwise me-1"></i>Generate from Meal Plan + </button> +</div> +{% endif %} + +<!-- Toast --> +<div class="position-fixed bottom-0 end-0 p-3 no-print" style="z-index:1100"> + <div id="toast" class="toast align-items-center text-bg-success border-0" role="alert"> + <div class="d-flex"> + <div class="toast-body" id="toastBody">Done!</div> + <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> +const weekStart = '{{ week_start.isoformat() }}'; +const toast = new bootstrap.Toast(document.getElementById('toast'), {delay: 2000}); + +// Checkbox toggle +document.querySelectorAll('.shop-check').forEach(cb => { + cb.addEventListener('change', async function() { + const item = this.closest('.shopping-item'); + const res = await api(`/shopping-list/${item.dataset.id}/check`, { + method: 'POST', body: JSON.stringify({checked: this.checked}), + }); + if (res) { + item.classList.toggle('checked', this.checked); + updateProgress(); + } + }); +}); + +function updateProgress() { + const all = document.querySelectorAll('.shop-check').length; + const done = document.querySelectorAll('.shop-check:checked').length; + const bar = document.querySelector('.progress-bar'); + if (bar) bar.style.width = all ? `${Math.round(done/all*100)}%` : '0%'; +} + +async function regenerate() { + const res = await api('/shopping-list/generate', { + method: 'POST', body: JSON.stringify({week: weekStart}), + }); + if (!res) return; + const data = await res.json(); + if (data.success) { + location.reload(); + } else { + document.getElementById('toastBody').textContent = data.error || 'No meals planned for this week.'; + document.getElementById('toast').className = 'toast align-items-center text-bg-danger border-0'; + toast.show(); + } +} + +document.getElementById('regenerateBtn')?.addEventListener('click', regenerate); +document.getElementById('regenerateBtn2')?.addEventListener('click', regenerate); + +document.getElementById('clearBtn')?.addEventListener('click', async () => { + if (!confirm('Clear the shopping list for this week?')) return; + const res = await api('/shopping-list/clear', { + method: 'POST', body: JSON.stringify({week: weekStart}), + }); + if (res) location.reload(); +}); +</script> +{% endblock %} |
