summaryrefslogtreecommitdiffstats
path: root/app/templates/recipe_detail.html
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken@claude>2026-05-25 00:46:10 +0000
committerKen D'Ambrosio <ken@claude>2026-05-25 00:46:10 +0000
commit55bcec90c14db6f2956ed51cf4df1503c0767f81 (patch)
treef25bfb8c46366b5d3dc6b4f66e242c65094b4ada /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.html290
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 %}