{% extends "base.html" %}
{% block title %}{{ tr.t("nav-recipes") }} - Cook{% endblock %}
{% block content %}
<div>
{% if !breadcrumbs.is_empty() %}
<nav class="mb-4">
<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>
<a href="{{ prefix }}/directory/{{ crumb.path }}{% if static_mode %}.html{% endif %}" class="hover:text-text">{{ crumb.name }}</a>
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
<div class="flex items-center justify-between mb-8">
<h1 class="text-display font-bold text-text">
{{ current_name }}
</h1>
{% if !static_mode %}
<a href="{{ new_recipe_url }}" class="btn btn-primary">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
{{ tr.t("new-recipe") }}
</a>
{% endif %}
</div>
<div id="sort-controls" class="hidden items-center gap-2 mb-6">
<label for="sort-field" class="text-sm text-muted">{{ tr.t("sort-by") }}</label>
<select id="sort-field" class="select">
<option value="name">{{ tr.t("sort-name") }}</option>
<option value="modified">{{ tr.t("sort-modified") }}</option>
<option value="created" id="sort-created-option">{{ tr.t("sort-created") }}</option>
</select>
<button id="sort-dir" type="button" class="btn" aria-label="{{ tr.t("sort-direction-toggle") }}" title="{{ tr.t("sort-direction-toggle") }}">↑</button>
</div>
{% match todays_menu %}
{% when Some with (menu) %}
<div class="mb-8 card p-6 border-l-[3px] border-l-accent">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-2xl">📋</span>
<h2 class="text-title font-bold text-text">{{ tr.t("todays-menu-title") }}</h2>
<span class="text-sm text-faint">{{ menu.date_display }}</span>
</div>
<p class="text-sm text-muted">{{ tr.t("todays-menu-from") }}: {{ menu.menu_name }}</p>
</div>
<a href="{{ prefix }}/{% if static_mode %}menu{% else %}recipe{% endif %}/{{ menu.menu_path }}{% if static_mode %}.html{% endif %}" class="btn self-start sm:self-center">
{{ tr.t("todays-menu-view") }}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
{% when None %}
{% endmatch %}
<div id="recipes-grid" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for item in items %}
{% if item.is_directory %}
<a href="{{ prefix }}/directory/{{ item.path }}{% if static_mode %}.html{% endif %}" data-type="directory" data-name="{{ item.name }}" class="recipe-card">
<div class="p-6 flex-1">
<div class="recipe-card-icon">
<span>📁</span>
</div>
<h2 class="recipe-card-title">{{ item.name }}</h2>
{% match item.count %}
{% when Some with (count) %}
<span class="text-sm text-accent-text font-medium">{{ tr.tn("recipes-count", count) }}</span>
{% when None %}
{% endmatch %}
</div>
</a>
{% else %}
<a href="{{ prefix }}/{% if static_mode %}{% if item.is_menu %}menu{% else %}recipe{% endif %}{% else %}recipe{% endif %}/{{ item.path }}{% if static_mode %}.html{% endif %}"
data-type="recipe"
data-name="{{ item.name }}"
{% if let Some(ts) = item.modified_at %}data-modified="{{ ts }}"{% endif %}
{% if let Some(ts) = item.created_at %}data-created="{{ ts }}"{% endif %}
class="recipe-card">
{% match item.image_path %}
{% when Some with (img) %}
<div class="h-48 bg-sunk overflow-hidden">
<img src="{{ img }}" alt="{{ item.name }}" class="w-full h-full object-cover">
</div>
<div class="p-6">
{% when None %}
<div class="p-6">
<div class="recipe-card-icon">
<span>{% if item.is_menu %}📋{% else %}🍽️{% endif %}</span>
</div>
{% endmatch %}
<div class="flex flex-wrap items-center gap-2">
<h2 class="recipe-card-title">{{ item.name }}</h2>
{% if item.is_menu %}
<span class="tag mb-2">{{ tr.t("recipe-type-menu") }}</span>
{% endif %}
</div>
{% match item.description %}
{% when Some with (description) %}
<p class="text-muted text-sm">{{ description }}</p>
{% when None %}
{% endmatch %}
{% if !item.tags.is_empty() %}
<div class="mt-3 flex flex-wrap gap-2">
{% for tag in item.tags.iter().take(3) %}
<span class="tag">{{ tag }}</span>
{% endfor %}
{% if item.tags.len() > 3 %}
<span class="text-xs text-faint">+{{ item.tags.len() - 3 }}</span>
{% endif %}
</div>
{% endif %}
</div>
</a>
{% endif %}
{% endfor %}
{% if items.is_empty() %}
<div class="text-faint text-center py-8">
{{ tr.t("recipes-empty") }}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
const grid = document.getElementById('recipes-grid');
const controls = document.getElementById('sort-controls');
const fieldSelect = document.getElementById('sort-field');
const dirBtn = document.getElementById('sort-dir');
const createdOption = document.getElementById('sort-created-option');
if (!grid || !controls || !fieldSelect || !dirBtn) return;
const STORE_KEY = 'recipes-sort';
let sortField = 'name';
let sortDir = 'asc';
const recipeCards = Array.from(grid.querySelectorAll('[data-type="recipe"]'));
const allHaveCreated = recipeCards.length > 0 &&
recipeCards.every(el => el.hasAttribute('data-created'));
if (!allHaveCreated && createdOption) {
createdOption.remove();
}
if (recipeCards.length < 2) return;
controls.classList.remove('hidden');
controls.classList.add('flex');
function restore() {
let saved = null;
try {
saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null');
} catch (e) {
}
if (!saved) return;
const valid = Array.from(fieldSelect.options).some(o => o.value === saved.field);
if (!valid) return;
sortField = saved.field;
fieldSelect.value = saved.field;
if (saved.dir === 'asc' || saved.dir === 'desc') {
sortDir = saved.dir;
}
}
function persist() {
try {
sessionStorage.setItem(STORE_KEY, JSON.stringify({ field: sortField, dir: sortDir }));
} catch (e) {
}
}
function getValue(el) {
if (sortField === 'name') {
return (el.getAttribute('data-name') || '').trim();
}
const raw = el.getAttribute(sortField === 'modified' ? 'data-modified' : 'data-created');
if (raw === null) return null;
const n = parseInt(raw, 10);
return Number.isNaN(n) ? null : n;
}
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
function compare(a, b) {
const av = getValue(a);
const bv = getValue(b);
if (av === null && bv === null) return 0;
if (av === null) return 1;
if (bv === null) return -1;
const cmp = typeof av === 'string' ? collator.compare(av, bv) : av - bv;
return sortDir === 'asc' ? cmp : -cmp;
}
function applySort() {
const all = Array.from(grid.children);
const dirs = all.filter(el => el.dataset.type === 'directory');
const recipes = all.filter(el => el.dataset.type === 'recipe');
const others = all.filter(el => !el.dataset.type);
recipes.sort(compare);
dirs.sort((a, b) => {
const cmp = collator.compare(a.getAttribute('data-name') || '', b.getAttribute('data-name') || '');
return sortField === 'name' && sortDir === 'desc' ? -cmp : cmp;
});
const frag = document.createDocumentFragment();
dirs.forEach(el => frag.appendChild(el));
recipes.forEach(el => frag.appendChild(el));
others.forEach(el => frag.appendChild(el));
grid.appendChild(frag);
dirBtn.textContent = sortDir === 'asc' ? '↑' : '↓';
}
fieldSelect.addEventListener('change', function () {
sortField = this.value;
sortDir = sortField === 'name' ? 'asc' : 'desc';
applySort();
persist();
});
dirBtn.addEventListener('click', function () {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
applySort();
persist();
});
restore();
if (sortField !== 'name' || sortDir !== 'asc') {
applySort();
}
})();
</script>
{% endblock %}