{% extends "base.html" %}
{% block title %}{{ name }} - Cook{% endblock %}
{% block content %}
<div id="menu-content">
<nav class="mb-4 print:hidden">
<ol class="flex items-center space-x-2 text-sm text-muted">
<li><a href="{{ prefix }}/{% if static_mode %}index.html{% endif %}" class="hover:text-text">{{ tr.t("nav-recipes") }}</a></li>
{% for crumb in breadcrumbs %}
<li class="flex items-center">
<span class="mx-2 text-faint">/</span>
{% if loop.last %}
<span class="text-text">{{ crumb }}</span>
{% else %}
<a href="{{ prefix }}/directory/{{ breadcrumbs[..loop.index0 + 1].join("/") }}{% if static_mode %}.html{% endif %}" class="hover:text-text">{{ crumb }}</a>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
<div class="mb-4">
{% match image_path %}
{% when Some with (img) %}
<div class="mb-4 max-w-4xl mx-auto print:hidden">
<div class="overflow-hidden card bg-sunk">
<img src="{{ img }}" alt="{{ name }}" class="w-full h-auto max-h-[500px] object-contain mx-auto">
</div>
</div>
{% when None %}
{% endmatch %}
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4 print:mb-1">
<div class="flex items-center gap-3">
<h1 class="text-display font-bold text-text print:text-2xl">{{ name }}</h1>
<span class="tag print:hidden">{{ tr.t("recipe-type-menu") }}</span>
</div>
{% if !static_mode %}
<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-muted mr-2">{{ tr.t("recipe-scale-label") }}:</label>
<div class="stepper">
<button type="button" aria-label="{{ tr.t("aria-decrease-scale") }}" onclick="adjustScale(-0.5)">−</button>
<input type="number"
id="scale"
value="{{ scale }}"
min="0.5"
max="200"
step="0.5"
onchange="goToScale(this.value)">
<button type="button" aria-label="{{ tr.t("aria-increase-scale") }}" onclick="adjustScale(0.5)">+</button>
</div>
</div>
<a href="{{ prefix }}/edit/{{ recipe_path }}" class="btn" title="{{ tr.t("action-edit") }}">
<svg 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>{{ tr.t("action-edit") }}</span>
</a>
<button onclick="addToShoppingList(event, {{ recipe_path|json }})" class="btn btn-primary" title="{{ tr.t("recipe-add-all-to-shopping") }}">
<svg 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>{{ tr.t("recipe-add-all-to-shopping") }}</span>
</button>
</div>
{% endif %}
</div>
<div class="space-y-2 print:hidden">
{% match metadata %}
{% when Some with (metadata) %}
{% match metadata.description %}
{% when Some with (description) %}
<div class="recipe-note">
<p class="m-0">{{ description }}</p>
</div>
{% when None %}
{% endmatch %}
<div id="metadata-container" class="flex flex-wrap gap-3">
{% 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.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-info hover:underline ml-1">{{ source_url|hostname }}</a></span>
{% when None %}
{% endmatch %}
{% for (key, value) in metadata.custom %}
<span class="metadata-pill metadata-custom">{{ key }}: {{ value }}</span>
{% endfor %}
</div>
{% when None %}
{% endmatch %}
</div>
</div>
<div class="space-y-6">
{% for section in sections %}
<div class="menu-section card overflow-hidden">
{% match section.name %}
{% when Some with (name) %}
<div class="menu-section-header bg-sunk border-b border-line px-6 py-3">
<h2 class="text-title font-semibold text-text">{{ name }}</h2>
</div>
{% when None %}
{% endmatch %}
<div class="menu-section-body p-6 space-y-2">
{% for line in section.lines %}
{% if line.len() == 1 %}
{% match line[0] %}
{% when crate::web::templates::MenuSectionItem::Text with (text) %}
{% if text.trim().ends_with(":") %}
<h3 class="text-title font-semibold text-text mt-2 mb-1">{{ text }}</h3>
{% else %}
<div class="pl-6">
<span class="text-text">{{ text }}</span>
</div>
{% endif %}
{% when _ %}
<div class="pl-6 flex items-baseline gap-x-2">
{% match line[0] %}
{% when crate::web::templates::MenuSectionItem::RecipeReference with { name, scale } %}
<span class="inline-flex items-center gap-1">
<a href="{{ prefix }}/recipe/{{ name.strip_prefix("./").unwrap_or(name) }}{% if static_mode %}.html{% endif %}"
class="text-info font-medium hover:underline">
{{ name.strip_prefix("./").unwrap_or(name).replace("/", " › ") }}
</a>
{% match scale %}
{% when Some with (s) %}
<span class="text-sm text-faint">(×{{ s }})</span>
{% when None %}
{% endmatch %}
</span>
{% when crate::web::templates::MenuSectionItem::Ingredient with { name, quantity, unit } %}
<span class="inline-flex items-center gap-1">
<span class="ingredient-badge">
{{ name }}
{% match quantity %}
{% when Some with (q) %}
<span>{{ q }}</span>
{% when None %}
{% endmatch %}
{% match unit %}
{% when Some with (u) %}
<span class="font-normal text-muted">{{ u }}</span>
{% when None %}
{% endmatch %}
</span>
</span>
{% when _ %}
{% endmatch %}
</div>
{% endmatch %}
{% else %}
<div class="pl-6 flex flex-wrap items-baseline gap-x-2">
{% for item in line %}
{% match item %}
{% when crate::web::templates::MenuSectionItem::Text with (text) %}
<span class="text-text">{{ text }}</span>
{% when crate::web::templates::MenuSectionItem::RecipeReference with { name, scale } %}
<span class="inline-flex items-center gap-1">
<a href="{{ prefix }}/recipe/{{ name.strip_prefix("./").unwrap_or(name) }}{% if static_mode %}.html{% endif %}"
class="text-info font-medium hover:underline">
{{ name.strip_prefix("./").unwrap_or(name).replace("/", " › ") }}
</a>
{% match scale %}
{% when Some with (s) %}
<span class="text-sm text-faint">(×{{ s }})</span>
{% when None %}
{% endmatch %}
</span>
{% when crate::web::templates::MenuSectionItem::Ingredient with { name, quantity, unit } %}
<span class="inline-flex items-center gap-1">
<span class="ingredient-badge">
{{ name }}
{% match quantity %}
{% when Some with (q) %}
<span>{{ q }}</span>
{% when None %}
{% endmatch %}
{% match unit %}
{% when Some with (u) %}
<span class="font-normal text-muted">{{ u }}</span>
{% when None %}
{% endmatch %}
</span>
</span>
{% endmatch %}
{% endfor %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% if !static_mode %}
<script>
function escHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const RECIPE_URL = {{ prefix|json|safe }} + '/recipe/' + {{ recipe_path|json|safe }};
const DEFAULT_SCALE = {{ scale }};
function goToScale(value) {
const input = document.getElementById('scale');
const n = parseFloat(value);
if (!Number.isFinite(n)) {
input.value = DEFAULT_SCALE;
return;
}
const min = parseFloat(input.min) || 0.5;
const max = parseFloat(input.max) || 200;
const clamped = Math.min(max, Math.max(min, n));
window.location.href = RECIPE_URL + '?scale=' + encodeURIComponent(clamped);
}
async function addToShoppingList(event, recipePath) {
event.preventDefault();
const button = event.currentTarget;
const originalText = button.innerHTML;
try {
const scale = document.getElementById('scale')?.value || 1;
const response = await fetch('{{ prefix }}/api/shopping_list/add_menu', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: recipePath,
scale: parseFloat(scale)
})
});
if (response.ok) {
button.innerHTML = '<svg 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> ' + escHtml({{ tr.t("recipe-added")|json|safe }});
button.classList.add('bg-ok-soft', 'text-ok', 'border-ok');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('bg-ok-soft', 'text-ok', 'border-ok');
}, 2000);
} else {
throw new Error({{ tr.t("shopping-failed-to-add")|json|safe }});
}
} catch (error) {
console.error('Error:', error);
button.innerHTML = '<svg 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> ' + escHtml({{ tr.t("shopping-error")|json|safe }});
button.classList.add('bg-danger-soft', 'text-danger', 'border-danger');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('bg-danger-soft', 'text-danger', 'border-danger');
}, 2000);
}
}
</script>
{% endif %}
{% endblock %}