{% extends "base.html" %}
{% block title %}{{ recipe.name }} - Cook{% endblock %}
{% block content %}
<div>
<nav class="mb-4 breadcrumb print:hidden">
<ol class="flex items-center space-x-2 text-sm text-gray-600">
<li><a href="{{ prefix }}/" class="hover:text-gray-900">{{ tr.t("nav-recipes") }}</a></li>
{% for crumb in breadcrumbs %}
<li class="flex items-center">
<span class="mx-2 text-gray-400">/</span>
{% if loop.last %}
<span class="text-gray-900">{{ crumb }}</span>
{% else %}
<a href="{{ prefix }}/directory/{{ breadcrumbs[..loop.index0 + 1].join("/") }}" class="hover:text-gray-900">{{ crumb }}</a>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
<div class="mb-8">
<!-- Recipe image if available -->
{% match image_path %}
{% when Some with (img) %}
<div class="mb-6 max-w-4xl mx-auto flex justify-center">
<img src="{{ img }}" alt="{{ recipe.name }}" class="rounded-xl shadow-lg max-w-full h-auto max-h-[500px]">
</div>
{% when None %}
{% endmatch %}
<!-- Title bar with scale and shopping list button -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6 print:flex-row print:items-center">
<h1 class="text-3xl md:text-4xl font-bold bg-gradient-to-r from-orange-600 to-yellow-600 bg-clip-text text-transparent print:text-2xl">
{{ recipe.name }}
</h1>
{% if scale != 1.0 %}
<div class="hidden print:block text-lg font-normal text-gray-700">{{ tr.t("recipe-scale-label") }}: {{ scale }}x</div>
{% endif %}
<div class="flex flex-wrap items-center gap-2 lg:gap-3 print:hidden">
<div class="flex items-center">
<label for="scale" class="text-sm font-medium text-gray-700 mr-2">{{ tr.t("recipe-scale-label") }}:</label>
<input type="number"
id="scale"
value="{{ scale }}"
min="0.5"
max="200"
step="0.5"
class="px-3 py-2 border border-gray-300 rounded-lg w-20 text-center"
onchange="window.location.href = `{{ prefix }}/recipe/{{ recipe_path }}?scale=${this.value}`">
</div>
<a href="{{ prefix }}/edit/{{ recipe_path }}"
class="px-3 lg:px-4 py-2 text-sm lg:text-base bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg hover:from-blue-600 hover:to-cyan-600 transition-all shadow-md flex items-center gap-1.5 lg:gap-2 whitespace-nowrap"
title="{{ tr.t("action-edit") }}">
<svg class="w-4 h-4 lg:w-5 lg:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
<span class="hidden lg:inline">{{ tr.t("action-edit") }}</span>
</a>
<button onclick="addToShoppingList(event, {{ recipe_path|json }})"
class="px-3 lg:px-4 py-2 text-sm lg:text-base bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg hover:from-purple-600 hover:to-pink-600 transition-all shadow-md flex items-center gap-1.5 lg:gap-2 whitespace-nowrap"
title="{{ tr.t("recipe-add-to-shopping") }}">
<svg class="w-4 h-4 lg:w-5 lg:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
<span class="hidden lg:inline">{{ tr.t("recipe-add-to-shopping") }}</span>
</button>
<button id="start-cooking-btn"
onclick="startCookingMode()"
class="px-3 lg:px-4 py-2 text-sm lg:text-base bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-lg hover:from-green-600 hover:to-emerald-600 transition-all shadow-md flex items-center gap-1.5 lg:gap-2 whitespace-nowrap print:hidden"
title="Start Cooking">
<svg class="w-4 h-4 lg:w-5 lg:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="hidden lg:inline">Cook</span>
</button>
</div>
</div>
<!-- Recipe info section -->
<div class="space-y-4">
<!-- Tags row -->
{% if !tags.is_empty() %}
<div class="flex flex-wrap gap-2">
{% for tag in tags %}
<span class="tag">#{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
{% match recipe.metadata %}
{% when Some with (metadata) %}
<!-- Description if present -->
{% match metadata.description %}
{% when Some with (description) %}
<div class="bg-gradient-to-r from-orange-50 to-yellow-50 p-4 rounded-xl border border-orange-200">
<p class="text-gray-700 italic">{{ description }}</p>
</div>
{% when None %}
{% endmatch %}
<!-- Metadata row -->
<div id="metadata-container" class="flex flex-wrap gap-3">
<!-- Display all metadata items -->
{% match metadata.servings %}
{% when Some with (servings) %}
<span class="metadata-pill metadata-servings">👥 {{ servings }} {{ tr.t("recipe-servings-label") }}</span>
{% when None %}
{% endmatch %}
{% match metadata.time %}
{% when Some with (time) %}
<span class="metadata-pill metadata-time">⏱️ {{ time }}</span>
{% when None %}
{% endmatch %}
{% match metadata.difficulty %}
{% when Some with (difficulty) %}
<span class="metadata-pill metadata-difficulty">📊 {{ difficulty }}</span>
{% when None %}
{% endmatch %}
{% match metadata.course %}
{% when Some with (course) %}
<span class="metadata-pill metadata-course">🍽️ {{ course }}</span>
{% when None %}
{% endmatch %}
{% match metadata.prep_time %}
{% when Some with (prep_time) %}
<span class="metadata-pill metadata-prep">⏱️ {{ tr.t("meta-prep-time") }}: {{ prep_time }}</span>
{% when None %}
{% endmatch %}
{% match metadata.cook_time %}
{% when Some with (cook_time) %}
<span class="metadata-pill metadata-cook">🔥 {{ tr.t("meta-cook-time") }}: {{ cook_time }}</span>
{% when None %}
{% endmatch %}
{% match metadata.cuisine %}
{% when Some with (cuisine) %}
<span class="metadata-pill metadata-cuisine">🌍 {{ cuisine }}</span>
{% when None %}
{% endmatch %}
{% match metadata.diet %}
{% when Some with (diet) %}
<span class="metadata-pill metadata-diet">🥗 {{ diet }}</span>
{% when None %}
{% endmatch %}
{% match metadata.author %}
{% when Some with (author) %}
<span class="metadata-pill metadata-author">👤 {{ author }}</span>
{% when None %}
{% endmatch %}
{% match metadata.source %}
{% when Some with (source) %}
<span class="metadata-pill metadata-source">📖 {{ source }}</span>
{% when None %}
{% endmatch %}
{% match metadata.source_url %}
{% when Some with (source_url) %}
<span class="metadata-pill metadata-source-url">🔗 <a href="{{ source_url }}" class="text-blue-600 hover:text-blue-800 hover:underline">{{ source_url|hostname }}</a></span>
{% when None %}
{% endmatch %}
<!-- Custom metadata -->
{% for (key, value) in metadata.custom %}
<span class="metadata-pill metadata-custom">{{ key }}: {{ value }}</span>
{% endfor %}
</div>
{% when None %}
{% endmatch %}
</div>
</div>
<div class="grid md:grid-cols-3 gap-8 mb-8">
<div class="md:col-span-1">
<div class="bg-white rounded-2xl shadow-lg p-6">
<h2 class="text-xl font-bold mb-4 text-orange-600">🥘 {{ tr.t("recipe-ingredients") }}</h2>
{% if sections.len() > 1 %}
<!-- Display ingredients grouped by section -->
{% for section in sections %}
{% if section.ingredients.len() > 0 %}
{% match section.name %}
{% when Some with (name) %}
<h3 class="text-lg font-semibold mt-4 mb-2 text-orange-700">{{ name }}</h3>
{% when None %}
{% if !loop.first %}<h3 class="text-lg font-semibold mt-4 mb-2 text-orange-700">{{ tr.t("recipe-main-section") }}</h3>{% endif %}
{% endmatch %}
<ul class="space-y-3 {% if !loop.last %}mb-4{% endif %}">
{% for ingredient in section.ingredients %}
<li class="flex justify-between items-center bg-gradient-to-r from-orange-50 to-yellow-50 rounded-lg px-3 py-2">
<div class="flex-1 flex items-center">
{% match ingredient.reference_path %}
{% when Some with (path) %}
<input type="checkbox" checked
class="ref-checkbox w-4 h-4 text-blue-600 rounded focus:ring-blue-500 mr-2 flex-shrink-0"
data-ref-path="{{ path }}"
title="{{ tr.t("shopping-include-in-list") }}">
<a href="{{ prefix }}/recipe/{{ path }}" class="font-medium text-blue-600 hover:text-blue-800 hover:underline flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
{{ ingredient.name }}
</a>
{% when None %}
<span class="font-medium">{{ ingredient.name }}</span>
{% endmatch %}
{% match ingredient.note %}
{% when Some with (note) %}
<span class="text-sm text-gray-600 italic ml-1 truncate max-w-xs inline-block" title="{{ note }}" aria-label="{{ tr.t("recipe-preparation") }}: {{ note }}">({{ note }})</span>
{% when None %}
{% endmatch %}
</div>
<span class="text-orange-700 font-semibold ml-2">
{% match ingredient.quantity %}
{% when Some with (quantity) %}{{ quantity }}{% when None %}{% endmatch %}
{% match ingredient.unit %}
{% when Some with (unit) %} {{ unit }}{% when None %}{% endmatch %}
</span>
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
{% else %}
<!-- Fallback to original flat list for recipes without sections -->
<ul class="space-y-3">
{% for ingredient in ingredients %}
<li class="flex justify-between items-center bg-gradient-to-r from-orange-50 to-yellow-50 rounded-lg px-3 py-2">
<div class="flex-1 flex items-center">
{% match ingredient.reference_path %}
{% when Some with (path) %}
<input type="checkbox" checked
class="ref-checkbox w-4 h-4 text-blue-600 rounded focus:ring-blue-500 mr-2 flex-shrink-0"
data-ref-path="{{ path }}"
title="{{ tr.t("shopping-include-in-list") }}">
<a href="{{ prefix }}/recipe/{{ path }}" class="font-medium text-blue-600 hover:text-blue-800 hover:underline flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
{{ ingredient.name }}
</a>
{% when None %}
<span class="font-medium">{{ ingredient.name }}</span>
{% endmatch %}
{% match ingredient.note %}
{% when Some with (note) %}
<span class="text-sm text-gray-600 italic ml-1 truncate max-w-xs inline-block" title="{{ note }}" aria-label="{{ tr.t("recipe-preparation") }}: {{ note }}">({{ note }})</span>
{% when None %}
{% endmatch %}
</div>
<span class="text-orange-700 font-semibold ml-2">
{% match ingredient.quantity %}
{% when Some with (quantity) %}{{ quantity }}{% when None %}{% endmatch %}
{% match ingredient.unit %}
{% when Some with (unit) %} {{ unit }}{% when None %}{% endmatch %}
</span>
</li>
{% endfor %}
</ul>
{% endif %}
{% if cookware.len() > 0 %}
<h2 class="text-xl font-bold mt-6 mb-4 text-green-600">🍳 {{ tr.t("recipe-cookware") }}</h2>
<ul class="cookware-list space-y-2">
{% for item in cookware %}
<li class="bg-gradient-to-r from-green-50 to-blue-50 rounded-lg px-3 py-2">
<span class="font-medium">{{ item.name }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
<div class="md:col-span-2">
<div class="bg-white rounded-2xl shadow-lg p-6">
{% for section in sections %}
{% match section.name %}
{% when Some with (name) %}
<h3 class="text-xl font-semibold mt-6 mb-4 text-orange-700 border-b-2 border-orange-200 pb-2">{{ name }}</h3>
{% when None %}
{% endmatch %}
<ol class="space-y-4 {% if !loop.first %}mt-4{% endif %}">
{% for item in section.items %}
{% match item %}
{% when crate::server::templates::RecipeSectionItem::Step with (step) %}
<li class="bg-gradient-to-r from-gray-50 to-orange-50 rounded-xl p-4">
<div class="flex flex-col gap-4">
{% match step.image_path %}
{% when Some with (img) %}
<img class="image-step" src="{{ img }}" />
{% when None %}
{% endmatch %}
<div class="flex gap-4">
<div class="step-number">{{ step.number }}</div>
<div class="flex-1">
<div class="text-gray-700 mb-2 leading-8">
{% for step_item in step.items %}
{% match step_item %}
{% when crate::server::templates::StepItem::Text with (text) %}{{ text }}{% when crate::server::templates::StepItem::Ingredient with { name, reference_path } %}{% match reference_path %}{% when Some with (path) %}<a href="{{ prefix }}/recipe/{{ path }}" class="ingredient-badge hover:underline" title="View recipe: {{ name }}">{{ name }}</a>{% when None %}<span class="ingredient-badge">{{ name }}</span>{% endmatch %}{% when crate::server::templates::StepItem::Cookware with (name) %}<span class="cookware-badge">{{ name }}</span>{% when crate::server::templates::StepItem::Timer with (name) %}<span class="timer-badge">⏱️ {{ name }}</span>{% when crate::server::templates::StepItem::Quantity with (qty) %}<span class="font-bold text-orange-600">{{ qty }}</span>{% when crate::server::templates::StepItem::LineBreak %}<br>{% endmatch %}{% endfor %}
</div>
{% if step.ingredients.len() > 0 %}
<div class="text-sm text-gray-600 mt-2 pl-4 border-l-2 border-orange-300">
{% for ing in step.ingredients %}
<span class="inline-block mr-3">
{{ ing.name }}{% match ing.quantity %}{% when Some with (q) %}: {{ q }}{% when None %}{% endmatch %}{% match ing.unit %}{% when Some with (u) %} {{ u }}{% when None %}{% endmatch %}{% match ing.note %}{% when Some with (note) %} <span class="italic text-gray-600 truncate max-w-[200px] inline-block align-bottom" title="{{ note }}" aria-label="{{ tr.t("recipe-preparation") }}: {{ note }}">({{ note }})</span>{% when None %}{% endmatch %}{% if !loop.last %},{% endif %}
</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</li>
{% when crate::server::templates::RecipeSectionItem::Note with (note) %}
<li class="bg-gradient-to-r from-blue-50 to-purple-50 rounded-xl p-4 mb-3 border-l-4 border-blue-400">
<div class="flex items-start gap-2">
<span class="text-blue-600">📝</span>
<p class="text-gray-700 italic" style="white-space: pre-line">{{ note }}</p>
</div>
</li>
{% endmatch %}
{% endfor %}
</ol>
{% endfor %}
</div>
</div>
</div>
</div>
<script id="cooking-mode-data" type="application/json">
{{ self.cooking_mode_json()|safe }}
</script>
<script>
async function addToShoppingList(event, recipePath) {
const scale = document.getElementById('scale').value;
// Hide any previous error
const existingError = document.getElementById('recipe-error-banner');
if (existingError) existingError.remove();
// Collect checked recipe references
const refCheckboxes = document.querySelectorAll('.ref-checkbox');
let payload = {
path: recipePath,
scale: parseFloat(scale)
};
if (refCheckboxes.length > 0) {
payload.included_references = Array.from(refCheckboxes)
.filter(cb => cb.checked)
.map(cb => cb.dataset.refPath);
}
try {
const response = await fetch('{{ prefix }}/api/shopping_list/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (response.ok) {
// Change button temporarily to show success
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = `
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
{{ tr.t("recipe-added") }}
`;
button.classList.add('bg-gradient-to-r', 'from-green-500', 'to-green-600');
button.classList.remove('from-purple-500', 'to-pink-500', 'hover:from-purple-600', 'hover:to-pink-600');
// Reset after 2 seconds
setTimeout(() => {
button.innerHTML = originalContent;
button.classList.remove('from-green-500', 'to-green-600');
button.classList.add('from-purple-500', 'to-pink-500', 'hover:from-purple-600', 'hover:to-pink-600');
}, 2000);
} else {
const data = await response.json().catch(() => ({}));
showRecipeError(data.error || {{ tr.t("shopping-failed-to-add")|json|safe }});
}
} catch (error) {
console.error('Failed to add to shopping list:', error);
showRecipeError({{ tr.t("shopping-failed-to-add")|json|safe }});
}
}
function showRecipeError(message) {
const existing = document.getElementById('recipe-error-banner');
if (existing) existing.remove();
const banner = document.createElement('div');
banner.id = 'recipe-error-banner';
banner.className = 'mb-4 bg-red-50 border border-red-200 rounded-xl p-4';
banner.innerHTML = `
<div class="flex items-start gap-3">
<div class="flex-shrink-0 text-red-500">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<p class="flex-1 text-red-700 text-sm font-mono whitespace-pre-wrap">${message}</p>
<button onclick="this.closest('#recipe-error-banner').remove()" class="text-red-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
`;
// Insert before the main content
const content = document.querySelector('.grid');
content.parentNode.insertBefore(banner, content);
}
</script>
{% endblock %}
{% block scripts %}
<link rel="stylesheet" href="{{ prefix }}/static/css/cooking-mode.css">
<script src="{{ prefix }}/static/js/cooking-mode.js"></script>
{% endblock %}