From 16bfb665789449a7223453ed068388e750fec344 Mon Sep 17 00:00:00 2001 From: Romain Hild Date: Mon, 8 Jun 2026 20:20:40 +0200 Subject: [PATCH 1/3] feat: add recipe sorting by name, modified date, and created date --- .../plans/2026-06-04-recipe-sorting.md | 370 ++++++++++++++++++ .../specs/2026-06-04-recipe-sorting-design.md | 82 ++++ locales/de-DE/recipes.ftl | 7 + locales/en-US/recipes.ftl | 7 + locales/es-ES/recipes.ftl | 7 + locales/eu-ES/recipes.ftl | 7 + locales/fr-FR/recipes.ftl | 7 + locales/nl-NL/recipes.ftl | 7 + locales/sv-SE/recipes.ftl | 7 + src/server/builders.rs | 24 +- src/server/templates.rs | 2 + templates/recipes.html | 98 ++++- 12 files changed, 618 insertions(+), 7 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-04-recipe-sorting.md create mode 100644 docs/superpowers/specs/2026-06-04-recipe-sorting-design.md diff --git a/docs/superpowers/plans/2026-06-04-recipe-sorting.md b/docs/superpowers/plans/2026-06-04-recipe-sorting.md new file mode 100644 index 00000000..376287f8 --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-recipe-sorting.md @@ -0,0 +1,370 @@ +# Recipe Sorting Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add client-side sorting (by name, last modified, creation date) to the recipes list page via a field dropdown + direction toggle. + +**Architecture:** Rust populates `modified_at`/`created_at` Unix timestamps on each `RecipeItem`; the Askama template embeds them as `data-*` attributes on recipe cards; inline JavaScript handles DOM reordering without any page reload. + +**Tech Stack:** Rust (Askama templates, std::fs::metadata), Tailwind CSS, vanilla JavaScript + +--- + +## File Map + +| File | Change | +|------|--------| +| `src/server/templates.rs` | Add `modified_at: Option` and `created_at: Option` to `RecipeItem` | +| `src/server/builders.rs` | Populate those fields from `std::fs::metadata` when building recipe items | +| `templates/recipes.html` | Add `data-modified`/`data-created`/`data-type` attributes to cards; add sort control UI; add sort JS | + +--- + +## Task 1: Add timestamp fields to `RecipeItem` + +**Files:** +- Modify: `src/server/templates.rs:469-479` + +- [ ] **Step 1: Add the two new fields to `RecipeItem`** + +Open `src/server/templates.rs`. Find `RecipeItem` (around line 469) and add `modified_at` and `created_at`: + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecipeItem { + pub name: String, + pub path: String, + pub is_directory: bool, + pub count: Option, + pub description: Option, + pub tags: Vec, + pub image_path: Option, + pub is_menu: bool, + pub modified_at: Option, + pub created_at: Option, +} +``` + +- [ ] **Step 2: Verify it compiles (errors expected in builders.rs — that's fine)** + +```bash +cd /Users/romain/Projects/cooklang/cookcli && cargo build 2>&1 | grep "error" +``` + +Expected: errors about missing fields in `RecipeItem { ... }` struct literals in `builders.rs`. That confirms the field was added and is now required. + +--- + +## Task 2: Populate timestamps in `build_recipes_template` + +**Files:** +- Modify: `src/server/builders.rs:64-108` + +- [ ] **Step 1: Extract timestamps alongside tags/image in the recipe branch** + +In `src/server/builders.rs`, find the block starting at line 64: + +```rust +// Extract tags, image, and is_menu if this is a recipe +let (tags, image_path, is_menu) = if let Some(ref recipe) = child.recipe { +``` + +Replace it with: + +```rust +// Extract tags, image, is_menu, and file timestamps if this is a recipe +let (tags, image_path, is_menu, modified_at, created_at) = if let Some(ref recipe) = child.recipe { + let img_path = recipe.title_image().clone().and_then(|img| { + if img.starts_with("http://") || img.starts_with("https://") { + Some(img) + } else { + // Make path relative to base and accessible via /api/static + let img_path = camino::Utf8Path::new(&img); + if let Ok(relative) = img_path.strip_prefix(base_path) { + Some(format!("{url_prefix}/api/static/{relative}")) + } else if !img_path.is_absolute() { + Some(format!("{url_prefix}/api/static/{img_path}")) + } else { + img_path + .file_name() + .map(|name| format!("{url_prefix}/api/static/{name}")) + } + } + }); + + let (modified_at, created_at) = recipe.path().map(|p| { + let meta = std::fs::metadata(p).ok(); + let modified = meta.as_ref() + .and_then(|m| m.modified().ok()) + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64); + let created = meta.as_ref() + .and_then(|m| m.created().ok()) + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64); + (modified, created) + }).unwrap_or((None, None)); + + (recipe.tags(), img_path, recipe.is_menu(), modified_at, created_at) +} else { + (Vec::new(), None, false, None, None) +}; +``` + +- [ ] **Step 2: Pass the new fields into the `RecipeItem` push** + +Still in `builders.rs`, find the `items.push(RecipeItem { ... })` call (around line 88) and add the two new fields: + +```rust +items.push(RecipeItem { + name: name.to_string(), + path: item_path, + is_directory: is_dir, + count: if is_dir { + count_recipes_tree(child) + } else { + None + }, + description: None, + tags, + image_path, + is_menu, + modified_at, + created_at, +}); +``` + +- [ ] **Step 3: Verify clean compile** + +```bash +cd /Users/romain/Projects/cooklang/cookcli && cargo build 2>&1 | grep "error" +``` + +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +cd /Users/romain/Projects/cooklang/cookcli +git add src/server/templates.rs src/server/builders.rs +git commit -m "feat: add modified_at and created_at timestamps to RecipeItem" +``` + +--- + +## Task 3: Embed data attributes and add sort controls to template + +**Files:** +- Modify: `templates/recipes.html` + +- [ ] **Step 1: Add `id` and `data-type` attributes to the grid and cards** + +In `templates/recipes.html`, find the grid opening tag (line 58): + +```html + + + {% match todays_menu %} +``` + +Insert the sort controls between the title row and the today's menu block: + +```html + + + + + {% match todays_menu %} +``` + +- [ ] **Step 3: Verify template renders (start dev server and check the page)** + +```bash +cd /Users/romain/Projects/cooklang/cookcli && cargo run -- server ./seed +``` + +Open http://localhost:9080 — the recipes grid should still render correctly with no visible change yet (sort controls are hidden by default until JS runs). + +- [ ] **Step 4: Commit** + +```bash +cd /Users/romain/Projects/cooklang/cookcli +git add templates/recipes.html +git commit -m "feat: add data attributes and sort controls markup to recipes template" +``` + +--- + +## Task 4: Add client-side sort JavaScript + +**Files:** +- Modify: `templates/recipes.html` — `{% block scripts %}` at the bottom + +- [ ] **Step 1: Add the sort script inside `{% block scripts %}`** + +At the bottom of `templates/recipes.html`, inside the existing `{% block scripts %}{% endblock %}` block, add: + +```html +{% block scripts %} + +{% endblock %} +``` + +- [ ] **Step 2: Test name sort** + +With the dev server running (`cargo run -- server ./seed`), open http://localhost:9080. +- Sort controls should be visible if there are ≥ 2 recipes. +- Select "Name" — recipes should be in A→Z order. +- Click ↑ to flip to ↓ — recipes should reverse to Z→A. + +- [ ] **Step 3: Test modified sort** + +- Select "Modified" in the dropdown — recipes should reorder by last-modified date, newest first (↓). +- Click the direction button to verify oldest-first order. +- Right-click a recipe card, inspect element — verify `data-modified` contains a plausible Unix timestamp (e.g. ~1700000000). + +- [ ] **Step 4: Test created sort (if available)** + +- If on macOS, the "Created" option should appear in the dropdown. Select it — recipes reorder by creation date. +- If on a Linux filesystem that doesn't support birth time, the option should not appear. + +- [ ] **Step 5: Test directory pinning** + +If any subdirectories exist in the seed recipes, verify they always appear before recipe cards regardless of the selected sort. + +- [ ] **Step 6: Commit** + +```bash +cd /Users/romain/Projects/cooklang/cookcli +git add templates/recipes.html +git commit -m "feat: add client-side recipe sorting by name, modified date, and created date" +``` + +--- + +## Self-Review Checklist + +- [x] **Spec coverage:** All spec sections covered — data fields (Task 1+2), template attributes (Task 3), sort controls UI (Task 3), JS logic with Created visibility, directory pinning, direction toggle (Task 4). +- [x] **No placeholders:** All steps contain complete code. +- [x] **Type consistency:** `modified_at: Option` / `created_at: Option` defined in Task 1, populated in Task 2, read as `data-modified`/`data-created` string attributes in Task 4 — consistent throughout. +- [x] **Static site:** Both `ui.rs` and `build/renderer.rs` call `build_recipes_template`, so the static builder gets timestamps for free. +- [x] **Empty state:** The empty-state `
` has no `data-type` attribute and is handled by the `others` bucket in `applySort()`, so it stays at the end without breaking the sort. diff --git a/docs/superpowers/specs/2026-06-04-recipe-sorting-design.md b/docs/superpowers/specs/2026-06-04-recipe-sorting-design.md new file mode 100644 index 00000000..7df2d72d --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-recipe-sorting-design.md @@ -0,0 +1,82 @@ +# Recipe Sorting — Design Spec + +**Date:** 2026-06-04 +**Status:** Approved + +## Overview + +Add client-side sorting to the recipes list page. Users can sort by name, last modified date, or creation date. The sort controls must scale gracefully to future sort options without consuming excessive space. + +## Scope + +- Recipes list page (`/` and `/directory/*`) +- Sorting applies to recipe cards only; directories always stay pinned at the top +- No changes to the recipe detail page, API, or search + +## Data Layer + +**`RecipeItem`** (`src/server/templates.rs`) gains two new fields: + +```rust +pub modified_at: Option, // Unix timestamp (seconds), None for directories +pub created_at: Option, // Unix timestamp (seconds), None for directories or if unsupported +``` + +**`build_recipes_template`** (`src/server/builders.rs`): after building each non-directory item, call `std::fs::metadata(path)` on the recipe file path and extract: +- `.modified()` → `SystemTime` → seconds since Unix epoch → `i64` +- `.created()` → same, but wrapped in an extra `Option` because `created()` is not available on all Linux filesystems + +Both are silently `None` on any error. Directories get `None` for both fields. + +## Template + +**`templates/recipes.html`**: + +1. Embed timestamps on each recipe card `` tag: + ```html + {% if let Some(ts) = item.modified_at %}data-modified="{{ ts }}"{% endif %} + {% if let Some(ts) = item.created_at %}data-created="{{ ts }}"{% endif %} + ``` + Directory cards get no date attributes. + +2. Add a sort control row between the title/new-recipe row and the grid: + ``` + Sort by: [dropdown ▾] [↑↓ toggle button] + ``` + The dropdown contains options: Name, Modified, Created. + The Created option is hidden by JS on page load if no card has a `data-created` attribute. + The control row is hidden entirely (`display:none`) when there is only one item or the list is empty. + +## JavaScript + +Inline ` +{% endblock %} From eb65ac8ccb916d2769d8232785a332171b7c1726 Mon Sep 17 00:00:00 2001 From: romainhild Date: Fri, 12 Jun 2026 15:25:58 +0200 Subject: [PATCH 2/3] chore: format code --- src/server/builders.rs | 78 ++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/src/server/builders.rs b/src/server/builders.rs index 720114e7..96b09b30 100644 --- a/src/server/builders.rs +++ b/src/server/builders.rs @@ -62,42 +62,54 @@ pub fn build_recipes_template(input: RecipesBuildInput<'_>) -> Result Date: Fri, 12 Jun 2026 17:09:55 +0200 Subject: [PATCH 3/3] test: fix accessibility label --- templates/recipes.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/recipes.html b/templates/recipes.html index d145a4d4..253e3285 100644 --- a/templates/recipes.html +++ b/templates/recipes.html @@ -33,7 +33,7 @@

- {{ tr.t("sort-by") }} +