Commit Graph

52 Commits

Author SHA1 Message Date
jbrechtel 7b64f9576b Fix 'btn is not defined' JS error in Record meal modal
Build and Deploy / build-and-deploy (push) Successful in 12m59s
When the trigger JS was updated to use querySelectorAll('.roux-record-trigger')
for dual-button support, the variable was renamed from 'btn' to 'btns' but the
submit handler still referenced 'btn'. This commit:

- Adds activeBtn tracking: the button that opened the modal is stored in
  activeBtn via 'this' in the click handler
- Uses activeBtn.dataset.filename in the submit handler (with null guard)
- Removes the stale btn reference
2026-05-26 07:44:50 -04:00
jbrechtel 65b096bc31 Landing page: use recipe images in cards, make cards clickable
Build and Deploy / build-and-deploy (push) Successful in 1m48s
- Card images now show the recipe's actual image when available
  (background-image: url(...) with background-size: contain to preserve
  aspect ratio without distortion), falling back to gradient placeholders
- Cards are now wrapped in <a href="/recipes/{filename}"> so clicking
  anywhere on a card navigates to the recipe page
- Added .contain CSS class for image-fit behavior and .lnd-card a for
  link styling
2026-05-26 07:43:04 -04:00
jbrechtel 162d2f8a1e Formats repo 2026-05-26 07:39:28 -04:00
jbrechtel 0643dda10d Update recipe view page layout to match roux-recipe-mockup.html
Build and Deploy / build-and-deploy (push) Failing after 1m40s
Layout changes:
- Nav matching landing page style (ROUX logo + Recipes + Cook History)
- Large serif title with inline editing preserved
- Subtitle from recipe description
- Meta strip: rounded background with last cook info + source link + Log meal button
- Full-width hero image with upload/replace overlay
- 2-column body grid (1.5fr ingredients+method | 1fr cook history rail)
- Flat ingredient list with quantities (no checkboxes)
- Numbered method paragraphs (no checkboxes, timers preserved)
- Cook history rail on right with + Add button
- Tags displayed below the grid
- Responsive: single column on mobile

Architecture:
- Removed: renderMetaBar, renderCookLog, renderRecentCookEntries,
  renderRecentEntry, renderEntry, renderIngredientGroup, renderIngredientRow,
  collectSectionIngredients, dedupFirst, renderMethodSections, renderMethodStep,
  isEmptyStep, isBlankText, isMethodSection, sectionMethodSteps,
  renderMarginalNote, collectNotes, bodyItemSteps, pairWith
- Added: recipePageStyles, rpHeroImage, rpLastCookInfo, formatFullDate,
  rpIngredientList, rpNamedIngredientSection, collectIngredients,
  rpIngredientItems, rpMethodList, rpRenderMethodStep, rpHistoryList,
  rpRenderHistoryEntry
- Updated cookLogJs to use .roux-record-trigger class for dual trigger buttons
- Removed unused imports (Data.Function, catMaybes)
- All hlint warnings fixed, 51 tests passing
2026-05-26 07:38:15 -04:00
jbrechtel d22c8a79d4 Add landing page with cook log integration
Build and Deploy / build-and-deploy (push) Failing after 1m15s
- New landing page (/) based on roux-landing-mockup.html layout
  - Shuffle card that suggests random recipes
  - Recently cooked shelf (driven by cook log DB)
  - Recently added shelf (by file modification time)
- Existing recipe index moved to /recipes
- Updated nav links throughout to point to /recipes
- All hlint warnings fixed
2026-05-26 07:15:38 -04:00
jbrechtel edb682c8bc fix: skip empty/whitespace-only steps so step numbering starts at 1
Build and Deploy / build-and-deploy (push) Failing after 8s
2026-05-26 06:59:07 -04:00
jbrechtel 4dd580a042 Formats repo
Build and Deploy / build-and-deploy (push) Successful in 13m25s
2026-05-26 06:45:02 -04:00
jbrechtel ee42bfd981 fix: darken text colors — define --roux-muted, add explicit color to Cancel button, darken recent entry comment/date colors
Build and Deploy / build-and-deploy (push) Failing after 9s
2026-05-26 06:44:23 -04:00
jbrechtel 92ec058114 feat: show most recent 5 cook entries under Record meal button
Build and Deploy / build-and-deploy (push) Failing after 1m17s
2026-05-26 06:40:24 -04:00
jbrechtel 14f43a4ae9 fix: send URL-encoded form data instead of multipart, show errors inline, default empty date to today
Build and Deploy / build-and-deploy (push) Successful in 15m46s
2026-05-25 22:26:53 -04:00
jbrechtel 930fba369c feat: replace inline cook log form with 'Record meal' button + modal
Build and Deploy / build-and-deploy (push) Successful in 17m20s
2026-05-25 22:20:00 -04:00
jbrechtel ac7a979fe9 Formats repo
Build and Deploy / build-and-deploy (push) Successful in 15m3s
2026-05-25 22:11:28 -04:00
jbrechtel 90971568a3 feat: add cook history nav link to index page 2026-05-25 22:01:24 -04:00
jbrechtel c6a9c2dee4 feat: wire up cook log DB at server startup 2026-05-25 22:00:45 -04:00
jbrechtel ed015d3799 Fix all 20 hlint warnings across 5 source files
Build and Deploy / build-and-deploy (push) Successful in 12m38s
- Use newtype instead of data for single-field types (RouxConfig, ImportError)
- Replace maybe "" id with fromMaybe "" throughout
- Replace not (x `elem` ys) with x `notElem` ys
- Remove redundant brackets, redundant $, and eta-reduce lookupRecipe
- Use numeric underscore for 1_000_000
2026-05-25 21:30:21 -04:00
jbrechtel 7b51c617de feat: add inline title editing JavaScript 2026-05-21 21:29:54 -04:00
jbrechtel 1f7b55af6f feat: add CSS class and styles for editable recipe title 2026-05-21 21:29:20 -04:00
jbrechtel a509f44300 feat: add recipe image upload with in-place metadata update
Build and Deploy / build-and-deploy (push) Successful in 2m4s
Users can now upload an photo for any recipe directly from the
view page. The upload button appears as either a 'Replace' overlay
on existing images or an empty dashed placeholder for recipes
without one.

Upload flow:
1. Click upload button -> file picker opens (accepts image/*)
2. JavaScript reads the file as base64, POSTs to /upload-image
   with form fields: filename, file-b64, file-name
3. Server decodes the base64, saves to <recipe-dir>/<basename>.<ext>
4. Server updates the .cook file's YAML front matter, adding or
   updating the 'image:' key with a /recipe-images/ URL
5. Server returns JSON success, page reloads to show the image

Server routes added:
- POST /upload-image  — accepts base64-encoded image upload
- GET /recipe-images/<filename> — serves saved recipe images
  (extension-whitelisted to .jpg/.jpeg/.png/.gif/.webp)

Image files are stored alongside .cook files in the recipe directory
and served via the /recipe-images/ path. The fsnotify watcher detects
the .cook metadata change and triggers an SSE reload.
2026-05-21 20:43:58 -04:00
jbrechtel 439f5780c9 feat: show recipe image thumbnail on index page
Build and Deploy / build-and-deploy (push) Successful in 1m43s
2026-05-21 15:19:14 -04:00
jbrechtel d9a7f6538e feat: add Import link to index page navbar
Build and Deploy / build-and-deploy (push) Successful in 1m32s
2026-05-21 10:56:35 -04:00
jbrechtel c36e632d25 style: style import form inputs to match search box
Build and Deploy / build-and-deploy (push) Successful in 1m32s
2026-05-21 10:52:34 -04:00
jbrechtel 232373cd5c feat: expand import form with file upload via base64 JS 2026-05-21 10:32:49 -04:00
jbrechtel 5abf5490b4 feat: add interactive countdown timers to recipe steps
Build and Deploy / build-and-deploy (push) Successful in 1m45s
Cooklang timer references (~{15%minutes}) now render as clickable
spans. Clicking one starts a live countdown displayed in-place
with MM:SS format. A reset button (↺) restarts the timer from
the original duration. A close button (✕) returns to the static
label. When the timer reaches zero, the display pulses and turns
rust-colored for 3 cycles.

Implementation:
- durationToSeconds helper converts Duration to total seconds
- Timer span uses data-seconds attribute for the total
- Label + controls (display, reset, close) structure pre-rendered
  in HTML, toggled via a .running CSS class
- Timer JS initializes all .roux-timer-tag elements on page load
- Each timer manages its own setInterval, multiple timers can
  run simultaneously across different steps
2026-05-20 22:50:35 -04:00
jbrechtel 8807ba3851 feat: show recipe image alongside description when metadata has one
Build and Deploy / build-and-deploy (push) Successful in 1m30s
Adds a .roux-desc-row flex container that places the recipe
description text on the left and the image (from metaImage)
on the right, using what was previously empty horizontal space.
Image is capped at 280px, with a subtle shadow and rounded
corners. Responsively stacks on mobile.
2026-05-20 22:35:25 -04:00
jbrechtel 1b7f7b61aa fix: tighten typography, fix parser brace leak, polish UI
Build and Deploy / build-and-deploy (push) Successful in 1m40s
- Drop italics on cookware/timer tags — they were over-emphasizing
  roughly a third of step text. Now plain with slight opacity.
- Fix parser bug: @salt{large pinch} braces no longer leak through
  as raw text. Non-numeric quantities are consumed silently,
  ingredient appears without displayed quantity.
- Remove duplicate '\342\206\227 Original' link from marginalia SOURCE block.
  Title-adjacent link is sufficient.
- Add tags to marginalia column for visual balance.
- Constrain content to 1120px max-width for a printed-cookbook feel.
- Add .container h2 CSS specificity override to beat Pico CSS defaults.
  Section labels (INGREDIENTS, METHOD) now properly render in Fraunces.
- Switch navbars from <div> to <nav> so Pico's own nav ul{list-style:none}
  suppresses gray square bullets. Add explicit li{list-style:none} override.
- Constrain content to 1120px max-width so parchment frames the page.
2026-05-20 22:31:23 -04:00
jbrechtel 752918c1af fix: remove extra closing brace in index page JS
Build and Deploy / build-and-deploy (push) Successful in 1m26s
renderByCourse had a duplicate '}' that caused
'SyntaxError: missing ) in parenthetical' on load.
2026-05-20 22:18:17 -04:00
jbrechtel 78764be0a9 refactor(ui): overhaul typography, color palette, and recipe page layout
Build and Deploy / build-and-deploy (push) Successful in 1m35s
Changes based on design feedback:

Typography: Swap Lexend Peta → Fraunces (literary serif) for all
headings, preserving Quicksand for body text.

Color palette: Add --roux-ochre and --roux-sage vars. Restrict rust
accent to step numbers, rules, and hover. Titles and labels → ink.

Navbar: 'Roux' logo now uses Fraunces in ink color.

Parser bug: Add isMethodSection filter to exclude 'Ingredients'
sections from the method column, preventing raw text ingredient
listings from leaking into steps.

Index page: Titles in ink with rust-on-hover. Add letter dividers
(A, B, C...) for alpha mode. Show tags/course metadata under titles.

Recipe page: 3-column grid (240px 1fr 180px) with marginalia column
for source link and notes. Tag pills → sage outline. Ingredient
highlights → ochre underline. Checkboxes → hand-sketched rotation.
Source link → muted ink with rust hover.
2026-05-20 22:13:33 -04:00
jbrechtel d77f4eb59f fix: persist SSE debounce across page reloads via sessionStorage
Build and Deploy / build-and-deploy (push) Successful in 1m20s
The in-memory lastReload variable was lost on location.reload() because
the JavaScript context is destroyed. This caused a reload loop when the
watcher fired multiple SSE events for the same file change — each reload
reset the debounce to 0, so the next event would trigger another reload.

Switch to sessionStorage, which persists across page loads within the
same browser tab session. Also increases the debounce window to 3000ms
to account for page load time.
2026-05-20 15:42:01 -04:00
jbrechtel 4738972046 feat: add import form and result page templates 2026-05-20 13:07:31 -04:00
jbrechtel 795a99ae2b fix: deduplicate SSE events and debounce client reload
Build and Deploy / build-and-deploy (push) Successful in 1m39s
2026-05-20 11:54:06 -04:00
jbrechtel 71cef21daf feat: add SSE client JS for live recipe reload on detail and index pages
Build and Deploy / build-and-deploy (push) Successful in 1m39s
2026-05-20 10:26:50 -04:00
jbrechtel 8f8dbecea5 refactor: replace aeson ToJSON/FromJSON with json-fleece schemas
- RecipeSearchEntry: replace genericToJSON ToJSON with Fleece schema
  using object combinators (required, optionalNullable, list text)
- SchemaOrgRecipe: replace manual FromJSON instance with Fleece decoder
  that reuses existing helper functions for quirky fields
- Html.hs: use Fleece.Aeson.Encoder.encode instead of Data.Aeson.encode
- stack.yaml: add shrubbery + json-fleece extra-deps
- package.yaml: add json-fleece-aeson + json-fleece-core deps
- Remove unused LANGUAGE pragmas and redundant brackets
2026-05-19 23:24:24 -04:00
jbrechtel 431386b292 feat: style search input with color scheme and auto-focus on load
- Adds CSS for input[type=search] using theme colors (bg, text, accent)
- Adds autofocus attribute so search box is focused on page load
2026-05-19 22:20:15 -04:00
jbrechtel cef407c31b fix: escape single quotes in JS renderFlatList string
Haskell\'s \' within double-quoted string literals is interpreted as
just \' (no-op escape). Switched to \\' to produce \' in the JS
output, which correctly escapes the single quotes inside the
single-quoted JS string literal for the CSS class name.
2026-05-19 22:00:46 -04:00
jbrechtel 8105cee498 fix: use preEscapedText for inline JS script content
H.toHtml was escaping the JS content, causing the script tag to be empty.
H.preEscapedText preserves the text as-is, which is correct for script tags.
2026-05-19 21:55:21 -04:00
jbrechtel d5e9c7e0d3 feat: replace server-rendered index with JS-rendered shell page
- Rewrites indexPage to render a shell page with embedded JSON + JS
- Adds searchJs with vanilla JS for client-side list rendering, filtering, sorting
- Hash-based sort modes (#alpha, #tags, #course)
- Search filters by title and filename (case-insensitive)
- Fix: recipe links use /recipes/ prefix and encodeURIComponent for # encoding
- Fix: avoid double-render on initial page load
2026-05-19 21:40:02 -04:00
jbrechtel 954bad87bd feat: replace server-rendered index with JS-rendered shell page 2026-05-19 21:35:52 -04:00
jbrechtel 43e1299616 Formats code 2026-05-19 20:21:43 -04:00
jbrechtel 4055bf62c1 Extract Cooklang types into Data.CookLang module
- Create src/Data/CookLang.hs with all recipe types (Recipe, Section,
  StepItem, Ingredient, Cookware, Timer, RecipeRef, Quantity, Duration,
  Metadata, emptyMetadata)
- Data.CookLang has zero Roux imports — can be extracted to its own
  package
- Roux.Types now just re-exports from Data.CookLang
- Update all Roux.* modules and tests to import Data.CookLang directly
- All 30 tests passing, hlint clean
2026-05-19 15:59:59 -04:00
jbrechtel 262faca90e Render source metadata as '↗ Original recipe' link next to title
- When recipe has a source: field in YAML metadata, shows a link
  to the right of the recipe title
- Link opens in new tab with ↗ icon and 'Original recipe' text
- Hidden when no source is present
- Title row uses flexbox: title on left, source link on right
2026-05-19 15:31:12 -04:00
jbrechtel 3a1bc37b45 Formats repo 2026-05-19 15:27:39 -04:00
jbrechtel f902ec65b9 Natural ingredient quantity rendering
- showQuantity now produces natural text (4 cups, 125 g, 1/2 tsp)
  instead of Cooklang internal format (4%cup, 125%g, 1/2%tsp)
- Add pluralize helper: cups, tbsp, lbs, pinches, etc.
  Handles common units; unknown units stay unchanged
- Add formatFraction: displays 3/2 as '1 1/2', 1/2 as '1/2'
- No % signs in displayed quantities
2026-05-19 13:34:23 -04:00
jbrechtel 1e150e703e Replace Pompiere font with Lexend Peta for headings
- Updated Google Fonts import
- h1, h3 now use Lexend Peta (sans-serif, 400 weight, 0.02em letter-spacing)
- Updated utility class name
2026-05-19 10:57:37 -04:00
jbrechtel 5eafbbe14c Add YAML front matter parsing and use metadata titles in UI
- Add extractFrontMatter: parses YAML front matter (---...---) into
  a Metadata value, returns remaining body for Cooklang parsing
- Add parseYamlLines: extracts known keys (title, author, source,
  difficulty, cuisine, course, diet, tags, image, description)
  into canonical Metadata fields
- Add parseTagList: handles both [tag1, tag2] and tag1, tag2 formats
- Index page recipeItem now takes RecipeInfo and displays riTitle
  (from metadata) instead of raw filename
- All three sort modes (alpha, tag, course) use title-aware display
- OlivierSalad example shows its metadata title correctly
2026-05-19 10:15:14 -04:00
jbrechtel 9e15126374 Custom checkbox with dark checkmark
- Replace native checkbox with appearance:none custom styling
- Unchecked: transparent bg, terracotta border
- Checked: terracotta background, dark checkmark (✓) in #3D2C1E
- Same styling for both ingredient rows and method steps
2026-05-19 10:07:04 -04:00
jbrechtel 4f02599e63 Gray checkbox styling and checked-state dimming
- Checkbox: accent-color terracotta, border rgba(184,92,56,0.4),
  transparent background (not dark/black)
- Checked ingredient rows: :has(input:checked) → opacity 0.45
- Checked steps: :has(input:checked) → opacity 0.45
- Removed unused .step.done classes
2026-05-19 10:03:58 -04:00
jbrechtel 0bb198555b Use Quicksand for body, Pompiere for headings
- Import both Google Fonts in one @import
- body: Quicksand (sans-serif) for main content
- h1, h3: Pompiere (cursive) for titles and section headers
- h2: Quicksand 600 weight, uppercase for subsection labels
- Add .pompiere and .quicksand utility classes
2026-05-19 09:59:59 -04:00
jbrechtel d1582e9034 Recipe page restyled to match mockup design
- Add Pompiere font from Google Fonts
- Color theme: #F5EFE0 (bg), #3D2C1E (text), #B85C38 (accent)
- Two-column layout (ingredients | method) matching mockup
- Horizontal metadata bar: Serves, Prep, Cook, Total
- Course badge and description paragraph
- Ingredients: checkbox rows with quantity prefix, grouped by section
- Method: numbered steps with checkboxes
- Notes: ¶ ornament + paragraph style
- Index page: styled navbar with sort mode buttons
- Index page: styled recipe list with separators
- Custom CSS via internal stylesheet (no Pico CSS override except
  link for reset/utility classes)
- Add LambdaCase and TupleSections to default extensions
- All 26 tests passing, hlint clean
2026-05-19 09:54:08 -04:00
jbrechtel b0926ee60d Add three browse modes to index page: alphabetical, by tag, by course
- Add SortMode type (AlphaSort | TagSort | CourseSort) to Roux.Html
- Routes: / (alpha), /sorted/tags, /sorted/course
- AlphaSort: recipes sorted by title (from metadata) or filename
- TagSort: recipes grouped under tag headings, empty state when none
- CourseSort: recipes grouped by course/category metadata
- Navigation bar in index page with active-highlighted buttons
- Add metaCourse field to Metadata model (+ emptyMetadata update)
- Add nubOrd helper for deduplicating tag/course lists
- All 26 tests passing, hlint clean
2026-05-19 08:16:58 -04:00
jbrechtel 95a7521584 Fix all hlint warnings
- Html.hs: replace lambdas with section/composition where possible
- Html.hs: replace case/fromMaybe with fromMaybe
- Html.hs: add fromMaybe to imports, use unqualified
- Parser.hs: replace span with break (2 locations)
- Parser.hs: replace if/then/else with list comprehension
- Parser.hs: remove redundant catch-all patterns (break is exhaustive)
- Server.hs: eta reduce htmlResponse
- Add .hlint.yaml to suppress 'Avoid lambda' (suggested fix has
  precedence issues with blaze-html's infix (!) operator)
- All 26 tests passing, hlint: No hints, exit code 0
2026-05-19 07:25:59 -04:00