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/recipes.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/recipes.html')
| -rw-r--r-- | app/templates/recipes.html | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/app/templates/recipes.html b/app/templates/recipes.html new file mode 100644 index 0000000..92a13bb --- /dev/null +++ b/app/templates/recipes.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} +{% block title %}Recipes — Menu Planner{% endblock %} + +{% block content %} +<div class="d-flex align-items-center justify-content-between mb-3"> + <h1 class="h3 fw-bold mb-0">Recipe Library</h1> + <span class="badge bg-secondary fs-6">{{ recipes | length }} recipes</span> +</div> + +<!-- Filters --> +<div class="card shadow-sm mb-4"> + <div class="card-body"> + <form method="GET" class="row g-2 align-items-end"> + <div class="col-md-5"> + <input type="text" name="search" class="form-control" placeholder="Search recipes…" value="{{ search }}"> + </div> + <div class="col-md-3"> + <select name="cuisine" class="form-select"> + <option value="all" {% if cuisine=='all' %}selected{% endif %}>All Cuisines</option> + {% for c in cuisines %} + <option value="{{ c }}" {% if cuisine==c %}selected{% endif %}>{{ cuisine_emoji[c] }} {{ c }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-2"> + <select name="status" class="form-select"> + <option value="active" {% if status=='active' %}selected{% endif %}>Active</option> + <option value="favorited" {% if status=='favorited' %}selected{% endif %}>Favorites</option> + <option value="candidate" {% if status=='candidate' %}selected{% endif %}>Candidates</option> + <option value="ignored" {% if status=='ignored' %}selected{% endif %}>Ignored</option> + <option value="all" {% if status=='all' %}selected{% endif %}>All</option> + </select> + </div> + <div class="col-md-2"> + <select name="sort" class="form-select"> + <option value="name" {% if sort=='name' %}selected{% endif %}>Sort: Name</option> + <option value="total" {% if sort=='total' %}selected{% endif %}>Sort: Total Time</option> + <option value="prep" {% if sort=='prep' %}selected{% endif %}>Sort: Prep Time</option> + <option value="cook" {% if sort=='cook' %}selected{% endif %}>Sort: Cook Time</option> + <option value="rating" {% if sort=='rating' %}selected{% endif %}>Sort: Rating</option> + </select> + </div> + <div class="col-md-2"> + <button type="submit" class="btn btn-primary w-100"><i class="bi bi-funnel me-1"></i>Filter</button> + </div> + </form> + </div> +</div> + +<!-- Cuisine tabs shortcut --> +<div class="mb-3 d-flex gap-2 flex-wrap"> + <a href="/recipes?cuisine=all&status={{ status }}" class="btn btn-sm {% if cuisine=='all' %}btn-dark{% else %}btn-outline-secondary{% endif %}">All</a> + {% for c in cuisines %} + <a href="/recipes?cuisine={{ c }}&status={{ status }}" class="btn btn-sm {% if cuisine==c %}btn-dark{% else %}btn-outline-secondary{% endif %}"> + {{ cuisine_emoji[c] }} {{ c }} + </a> + {% endfor %} +</div> + +{% if recipes %} +<div class="row g-3"> + {% for r in recipes %} + <div class="col-sm-6 col-lg-4 col-xl-3"> + <div class="recipe-card card h-100 shadow-sm {% if r.status == 'ignored' %}opacity-50{% endif %}"> + <div class="card-body d-flex flex-column"> + <div class="d-flex align-items-start justify-content-between mb-2"> + <span class="cuisine-pill">{{ cuisine_emoji.get(r.cuisine, '') }} {{ r.cuisine }}</span> + {% if r.status == 'favorited' %} + <i class="bi bi-heart-fill text-danger"></i> + {% elif r.status == 'ignored' %} + <i class="bi bi-eye-slash text-muted"></i> + {% endif %} + </div> + <h5 class="card-title mb-1">{{ r.name }}</h5> + <p class="card-text text-muted small flex-grow-1">{{ r.description[:100] }}{% if r.description|length > 100 %}…{% endif %}</p> + {% if r.added_by %}<p class="text-muted small mb-1"><i class="bi bi-person me-1"></i>{{ r.added_by }}</p>{% endif %} + {% if r.rating %} + <div class="mb-1" style="color:#f59e0b;font-size:0.85rem"> + {% for i in range(1,6) %}<i class="bi bi-star{% if r.rating >= i %}-fill{% endif %}"></i>{% endfor %} + </div> + {% endif %} + <div class="nutrition-row mt-2 mb-3"> + <span class="nutri-badge"><i class="bi bi-lightning-charge"></i> {{ r.calories_per_serving|int }} cal</span> + <span class="nutri-badge"><i class="bi bi-circle-half"></i> {{ r.carbs_per_serving|int }}g carbs</span> + <span class="nutri-badge"><i class="bi bi-clock"></i> {{ r.prep_time + r.cook_time }} min</span> + </div> + <div class="d-flex gap-1 mt-auto"> + <a href="/recipes/{{ r.id }}" class="btn btn-sm btn-primary flex-grow-1">View</a> + <button class="btn btn-sm btn-outline-danger status-btn" data-id="{{ r.id }}" data-status="favorited" title="Favorite"> + <i class="bi bi-heart{% if r.status=='favorited' %}-fill{% endif %}"></i> + </button> + <button class="btn btn-sm btn-outline-secondary status-btn" data-id="{{ r.id }}" data-status="{% if r.status=='ignored' %}candidate{% else %}ignored{% endif %}" title="{% if r.status=='ignored' %}Restore{% else %}Ignore{% endif %}"> + <i class="bi bi-{% if r.status=='ignored' %}eye{% else %}eye-slash{% endif %}"></i> + </button> + </div> + </div> + </div> + </div> + {% endfor %} +</div> +{% else %} +<div class="text-center py-5"> + <i class="bi bi-search fs-1 text-muted"></i> + <p class="text-muted mt-2">No recipes found. Try adjusting the filters.</p> +</div> +{% endif %} +{% endblock %} + +{% block scripts %} +<script> +document.querySelectorAll('.status-btn').forEach(btn => { + btn.addEventListener('click', async function() { + const res = await api(`/recipes/${this.dataset.id}/status`, { + method: 'POST', + body: JSON.stringify({status: this.dataset.status}), + }); + if (res && (await res.json()).success) location.reload(); + }); +}); +</script> +{% endblock %} |
