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_detail.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_detail.html')
| -rw-r--r-- | app/templates/recipe_detail.html | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/app/templates/recipe_detail.html b/app/templates/recipe_detail.html new file mode 100644 index 0000000..65aa0b0 --- /dev/null +++ b/app/templates/recipe_detail.html @@ -0,0 +1,290 @@ +{% extends "base.html" %} +{% block title %}{{ recipe.name }} — Menu Planner{% endblock %} + +{% block content %} +<div class="mb-3 d-flex gap-2"> + <a href="/recipes" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Back to Recipes</a> + <button onclick="window.print()" class="btn btn-sm btn-outline-secondary"><i class="bi bi-printer me-1"></i>Print</button> + <button id="shareBtn" class="btn btn-sm btn-outline-secondary"><i class="bi bi-link-45deg me-1"></i>Copy Link</button> +</div> + +<div class="row g-4"> + <!-- Left: recipe info --> + <div class="col-lg-8"> + <div class="card shadow-sm"> + <div class="card-body"> + <div class="d-flex align-items-start justify-content-between mb-3"> + <div> + <span class="cuisine-pill mb-2 d-inline-block">{{ cuisine_emoji.get(recipe.cuisine, '') }} {{ recipe.cuisine }}</span> + <h1 class="h2 fw-bold mb-1">{{ recipe.name }}</h1> + <p class="text-muted">{{ recipe.description }}</p> + {% if recipe.added_by %} + <p class="text-muted small mb-0"><i class="bi bi-person me-1"></i>Added by {{ recipe.added_by }}</p> + {% endif %} + </div> + <div class="ms-3 text-center"> + {% if recipe.status == 'favorited' %} + <i class="bi bi-heart-fill text-danger fs-3"></i><br> + <small class="text-muted">Favorited</small> + {% endif %} + </div> + </div> + + <!-- Nutrition + time badges --> + <div class="d-flex flex-wrap gap-2 mb-4"> + <span class="info-badge bg-warning-subtle text-warning-emphasis"> + <i class="bi bi-lightning-charge-fill"></i> {{ recipe.calories_per_serving|int }} cal / serving + </span> + <span class="info-badge bg-info-subtle text-info-emphasis"> + <i class="bi bi-circle-half"></i> {{ recipe.carbs_per_serving|int }}g net carbs + </span> + {% if recipe.protein_per_serving %} + <span class="info-badge bg-success-subtle text-success-emphasis"> + <i class="bi bi-droplet-fill"></i> {{ recipe.protein_per_serving|int }}g protein + </span> + {% endif %} + {% if recipe.fat_per_serving %} + <span class="info-badge bg-secondary-subtle text-secondary-emphasis"> + <i class="bi bi-droplet"></i> {{ recipe.fat_per_serving|int }}g fat + </span> + {% endif %} + <span class="info-badge bg-light text-dark border"> + <i class="bi bi-clock"></i> Prep: {{ recipe.prep_time }} min + </span> + <span class="info-badge bg-light text-dark border"> + <i class="bi bi-fire"></i> Cook: {{ recipe.cook_time }} min + </span> + <span class="info-badge bg-light text-dark border"> + <i class="bi bi-people"></i> {{ recipe.servings }} servings + </span> + </div> + + <!-- Rating --> + <div class="mb-4 d-flex align-items-center gap-2"> + <span class="text-muted small">Your rating:</span> + <span class="star-rating" id="starRating" data-rating="{{ recipe.rating or 0 }}" {% if not current_user.is_authenticated %}title="Log in to rate"{% endif %}> + {% for i in range(1, 6) %} + <i class="bi bi-star{% if recipe.rating and recipe.rating >= i %}-fill{% endif %} star" + data-value="{{ i }}" {% if current_user.is_authenticated %}style="cursor:pointer"{% endif %}></i> + {% endfor %} + </span> + {% if recipe.rating %}<span class="text-muted small">({{ recipe.rating }}/5)</span>{% endif %} + </div> + + <!-- Ingredients --> + <h4 class="fw-semibold mb-3"><i class="bi bi-basket me-2"></i>Ingredients</h4> + {% set categories = ingredients | map(attribute='category') | list | unique | list %} + {% for cat in ['Meat & Poultry','Seafood','Dairy & Eggs','Produce','Pantry','Spices & Herbs'] %} + {% set cat_ings = ingredients | selectattr('category', 'equalto', cat) | list %} + {% if cat_ings %} + <div class="mb-3"> + <h6 class="text-muted text-uppercase small fw-bold mb-2">{{ cat }}</h6> + <ul class="ingredient-list"> + {% for ing in cat_ings %} + <li> + <span class="ing-qty"> + {% if ing.quantity == ing.quantity|int %}{{ ing.quantity|int }}{% else %}{{ ing.quantity }}{% endif %} + {{ ing.unit }} + </span> + {{ ing.name }} + </li> + {% endfor %} + </ul> + </div> + {% endif %} + {% endfor %} + + <!-- Instructions --> + <h4 class="fw-semibold mt-4 mb-3"><i class="bi bi-list-ol me-2"></i>Instructions</h4> + <div class="instructions-block"> + {% for line in recipe.instructions.strip().split('\n') %} + {% if line.strip() %} + <div class="instruction-step">{{ line.strip() }}</div> + {% endif %} + {% endfor %} + </div> + </div> + </div> + + <!-- Comments --> + <div class="card shadow-sm mt-4" id="comments"> + <div class="card-header fw-semibold"><i class="bi bi-chat-left-text me-2"></i>Comments + {% if comments %}<span class="badge bg-secondary ms-1">{{ comments|length }}</span>{% endif %} + </div> + <div class="card-body"> + {% for c in comments %} + <div class="mb-3 pb-3 {% if not loop.last %}border-bottom{% endif %}"> + <div class="d-flex justify-content-between align-items-start"> + <div> + <span class="fw-semibold">{{ c.username }}</span> + <span class="text-muted small ms-2">{{ c.created_at[:16].replace('T',' ') }}</span> + </div> + {% if current_user.is_authenticated and current_user.username == c.username %} + <form method="POST" action="/recipes/{{ recipe.id }}/comments/{{ c.id }}/delete" class="no-print"> + <button type="submit" class="btn btn-sm btn-link text-danger p-0" title="Delete"> + <i class="bi bi-trash"></i> + </button> + </form> + {% endif %} + </div> + <p class="mb-0 mt-1">{{ c.body }}</p> + </div> + {% else %} + <p class="text-muted mb-3 small">No comments yet.</p> + {% endfor %} + + {% if current_user.is_authenticated %} + <form method="POST" action="/recipes/{{ recipe.id }}/comments" class="no-print"> + <div class="mb-2"> + <textarea name="body" class="form-control" rows="2" placeholder="Add a comment…" required></textarea> + </div> + <button type="submit" class="btn btn-sm btn-primary">Post Comment</button> + </form> + {% else %} + <p class="text-muted small mb-0"><a href="/login">Log in</a> to leave a comment.</p> + {% endif %} + </div> + </div> + </div> + + <!-- Right: actions --> + <div class="col-lg-4 no-print"> + {% if current_user.is_authenticated %} + <div class="card shadow-sm mb-3"> + <div class="card-body"> + <h5 class="fw-semibold mb-3">Add to Meal Plan</h5> + <div class="mb-2"> + <label class="form-label small text-muted">Date</label> + <input type="date" id="mealDate" class="form-control"> + </div> + <div class="row g-2 mb-3"> + <div class="col-7"> + <label class="form-label small text-muted">Meal</label> + <select id="mealType" class="form-select"> + <option value="breakfast">Breakfast</option> + <option value="lunch">Lunch</option> + <option value="dinner" selected>Dinner</option> + </select> + </div> + <div class="col-5"> + <label class="form-label small text-muted">People</label> + <input type="number" id="mealServings" class="form-control" min="1" max="20" value="2"> + </div> + </div> + <button id="addToMealPlan" class="btn btn-primary w-100"> + <i class="bi bi-plus-circle me-1"></i>Add to Plan + </button> + <div id="addResult" class="mt-2 d-none"></div> + </div> + </div> + + <div class="card shadow-sm"> + <div class="card-body"> + <h5 class="fw-semibold mb-3">Recipe Status</h5> + <div class="d-flex flex-column gap-2"> + <button class="btn {% if recipe.status=='favorited' %}btn-danger{% else %}btn-outline-danger{% endif %} status-action" data-status="favorited"> + <i class="bi bi-heart{% if recipe.status=='favorited' %}-fill{% endif %} me-2"></i> + {% if recipe.status=='favorited' %}Favorited{% else %}Add to Favorites{% endif %} + </button> + <button class="btn {% if recipe.status=='candidate' %}btn-success{% else %}btn-outline-success{% endif %} status-action" data-status="candidate"> + <i class="bi bi-check-circle{% if recipe.status=='candidate' %}-fill{% endif %} me-2"></i> + {% if recipe.status=='candidate' %}Candidate{% else %}Mark as Candidate{% endif %} + </button> + <button class="btn {% if recipe.status=='ignored' %}btn-secondary{% else %}btn-outline-secondary{% endif %} status-action" data-status="ignored"> + <i class="bi bi-eye-slash{% if recipe.status=='ignored' %}-fill{% endif %} me-2"></i> + {% if recipe.status=='ignored' %}Ignored{% else %}Ignore Recipe{% endif %} + </button> + </div> + </div> + </div> + {% else %} + <div class="card shadow-sm"> + <div class="card-body text-center text-muted py-4"> + <i class="bi bi-lock fs-2 mb-2 d-block"></i> + <p class="mb-2 small">Log in to add this to your meal plan or update its status.</p> + <a href="/login" class="btn btn-primary btn-sm">Log in</a> + </div> + </div> + {% endif %} + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> +const recipeId = {{ recipe.id }}; + +// Default date to today +document.getElementById('mealDate').value = new Date().toISOString().split('T')[0]; + +document.getElementById('addToMealPlan').addEventListener('click', async () => { + const mealDate = document.getElementById('mealDate').value; + const mealType = document.getElementById('mealType').value; + const servings = parseInt(document.getElementById('mealServings').value) || 2; + if (!mealDate) return; + const res = await api('/meal-plan/add', { + method: 'POST', + body: JSON.stringify({date: mealDate, meal_type: mealType, recipe_id: recipeId, servings}), + }); + if (!res) return; + const data = await res.json(); + const el = document.getElementById('addResult'); + el.classList.remove('d-none','alert-danger'); + if (data.success) { + el.className = 'mt-2 alert alert-success py-2 small'; + el.textContent = `Added to ${mealType} on ${mealDate} for ${servings} people`; + } else { + el.className = 'mt-2 alert alert-danger py-2 small'; + el.textContent = data.error || 'Error adding to plan'; + } +}); + +document.querySelectorAll('.status-action').forEach(btn => { + btn.addEventListener('click', async function() { + const res = await api(`/recipes/${recipeId}/status`, { + method: 'POST', + body: JSON.stringify({status: this.dataset.status}), + }); + if (res && (await res.json()).success) location.reload(); + }); +}); + +// Share / copy link +document.getElementById('shareBtn').addEventListener('click', function() { + navigator.clipboard.writeText(window.location.href).then(() => { + this.innerHTML = '<i class="bi bi-check2 me-1"></i>Copied!'; + setTimeout(() => { this.innerHTML = '<i class="bi bi-link-45deg me-1"></i>Copy Link'; }, 2000); + }); +}); + +// Star rating +const starWidget = document.getElementById('starRating'); +if (starWidget) { + const stars = starWidget.querySelectorAll('.star'); + const isAuth = {{ 'true' if current_user.is_authenticated else 'false' }}; + if (isAuth) { + stars.forEach(star => { + star.addEventListener('mouseover', function() { + const val = parseInt(this.dataset.value); + stars.forEach(s => { + s.className = 'bi star ' + (parseInt(s.dataset.value) <= val ? 'bi-star-fill' : 'bi-star'); + }); + }); + star.addEventListener('mouseout', function() { + const cur = parseInt(starWidget.dataset.rating) || 0; + stars.forEach(s => { + s.className = 'bi star ' + (parseInt(s.dataset.value) <= cur ? 'bi-star-fill' : 'bi-star'); + }); + }); + star.addEventListener('click', async function() { + const val = parseInt(this.dataset.value); + const res = await api(`/recipes/${recipeId}/rate`, { + method: 'POST', body: JSON.stringify({rating: val}), + }); + if (res) location.reload(); + }); + }); + } +} +</script> +{% endblock %} |
