feat: replace inline cook log form with 'Record meal' button + modal
Build and Deploy / build-and-deploy (push) Successful in 17m20s

This commit is contained in:
2026-05-25 22:20:00 -04:00
parent ac7a979fe9
commit 930fba369c
+71 -25
View File
@@ -229,6 +229,22 @@ page title content =
, ".roux-step:has(input:checked) .step-text { opacity: 0.4; }" , ".roux-step:has(input:checked) .step-text { opacity: 0.4; }"
, "" , ""
, ".roux-course { font-size: 0.6rem; color: var(--roux-accent); opacity: 0.65; margin: 0 0 0.15rem; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; }" , ".roux-course { font-size: 0.6rem; color: var(--roux-accent); opacity: 0.65; margin: 0 0 0.15rem; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; }"
, ""
, ".roux-record-btn { font-size: 0.8rem; padding: 0.4rem 0.8rem; background: var(--roux-accent); color: #fff; border: none; border-radius: 4px; cursor: pointer; white-space: nowrap; font-family: \"Quicksand\", sans-serif; font-weight: 500; transition: opacity 0.15s; }"
, ".roux-record-btn:hover { opacity: 0.85; }"
, ""
, "/* Record meal modal */"
, ".roux-modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); z-index: 999; }"
, ".roux-modal-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: var(--roux-paper); border-radius: 8px; padding: 1.5rem; z-index: 1000; min-width: 320px; box-shadow: 0 4px 24px rgba(61,44,30,0.2); }"
, ".roux-modal-dialog h3 { margin: 0 0 1rem; font-size: 1.1rem; }"
, ".roux-modal-dialog label { display: block; font-size: 0.8rem; font-weight: 500; margin-bottom: 0.3rem; color: var(--roux-text); opacity: 0.7; }"
, ".roux-modal-dialog input, .roux-modal-dialog textarea { width: 100%; background: var(--roux-bg); border: 1px solid rgba(61,44,30,0.2); border-radius: 4px; padding: 0.5rem; font-family: \"Quicksand\", sans-serif; font-size: 0.9rem; color: var(--roux-text); margin-bottom: 0.75rem; }"
, ".roux-modal-dialog textarea { min-height: 80px; resize: vertical; }"
, ".roux-modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.5rem; }"
, ".roux-modal-cancel { font-size: 0.8rem; padding: 0.4rem 0.8rem; background: transparent; border: 1px solid rgba(61,44,30,0.2); border-radius: 4px; cursor: pointer; font-family: \"Quicksand\", sans-serif; transition: opacity 0.15s; }"
, ".roux-modal-cancel:hover { opacity: 0.7; }"
, ".roux-modal-confirm { font-size: 0.8rem; padding: 0.4rem 0.8rem; background: var(--roux-accent); color: #fff; border: none; border-radius: 4px; cursor: pointer; font-family: \"Quicksand\", sans-serif; font-weight: 500; transition: opacity 0.15s; }"
, ".roux-modal-confirm:hover { opacity: 0.85; }"
, ".roux-desc { font-size: 0.85rem; line-height: 1.6; color: var(--roux-text); margin: 0; max-width: 640px; opacity: 0.85; }" , ".roux-desc { font-size: 0.85rem; line-height: 1.6; color: var(--roux-text); margin: 0; max-width: 640px; opacity: 0.85; }"
, ".roux-desc-row { display: flex; gap: 2rem; align-items: flex-start; margin-bottom: 1.5rem; }" , ".roux-desc-row { display: flex; gap: 2rem; align-items: flex-start; margin-bottom: 1.5rem; }"
, ".roux-desc-row .roux-desc { flex: 1; }" , ".roux-desc-row .roux-desc { flex: 1; }"
@@ -635,14 +651,38 @@ cookLogJs =
T.unlines T.unlines
[ "(function(){" [ "(function(){"
, "'use strict';" , "'use strict';"
, "var form = document.querySelector('.roux-cook-log-form');" , "var btn = document.getElementById('roux-record-btn');"
, "if (!form) return;" , "var modal = document.getElementById('roux-record-modal');"
, "var dateInput = form.querySelector('[name=cooked-date]');" , "var backdrop = document.getElementById('roux-modal-backdrop');"
, "if (dateInput && !dateInput.value) dateInput.value = new Date().toISOString().slice(0,10);" , "var cancel = document.getElementById('roux-modal-cancel');"
, "var form = document.getElementById('roux-record-form');"
, "var dateInput = document.getElementById('roux-record-date');"
, "var commentInput = document.getElementById('roux-record-comment');"
, "if (!btn || !modal || !form) return;"
, ""
, "function showModal() {"
, " dateInput.value = new Date().toISOString().slice(0,10);"
, " commentInput.value = '';"
, " modal.style.display = 'block';"
, " dateInput.focus();"
, "}"
, ""
, "function hideModal() {"
, " modal.style.display = 'none';"
, "}"
, ""
, "btn.addEventListener('click', showModal);"
, "if (cancel) cancel.addEventListener('click', hideModal);"
, "if (backdrop) backdrop.addEventListener('click', hideModal);"
, ""
, "document.addEventListener('keydown', function(e) {"
, " if (e.key === 'Escape' && modal.style.display === 'block') hideModal();"
, "});"
, ""
, "form.addEventListener('submit', async function(e) {" , "form.addEventListener('submit', async function(e) {"
, " e.preventDefault();" , " e.preventDefault();"
, " var filename = btn.dataset.filename;"
, " var fd = new FormData(form);" , " var fd = new FormData(form);"
, " var filename = form.dataset.filename;"
, " var res = await fetch('/cook-log/' + encodeURIComponent(filename), {" , " var res = await fetch('/cook-log/' + encodeURIComponent(filename), {"
, " method: 'POST'," , " method: 'POST',"
, " body: fd" , " body: fd"
@@ -652,6 +692,7 @@ cookLogJs =
, " alert(err.error || 'Failed to log cooking');" , " alert(err.error || 'Failed to log cooking');"
, " return;" , " return;"
, " }" , " }"
, " hideModal();"
, " var date = fd.get('cooked-date');" , " var date = fd.get('cooked-date');"
, " var comment = fd.get('comment');" , " var comment = fd.get('comment');"
, " var entryDiv = document.createElement('div');" , " var entryDiv = document.createElement('div');"
@@ -670,9 +711,8 @@ cookLogJs =
, " commentSpan.textContent = comment;" , " commentSpan.textContent = comment;"
, " entryDiv.appendChild(commentSpan);" , " entryDiv.appendChild(commentSpan);"
, " }" , " }"
, " var container = form.nextElementSibling;" , " var container = document.querySelector('.roux-cook-entries');"
, " if (container) container.prepend(entryDiv);" , " if (container) container.prepend(entryDiv);"
, " form.querySelector('[name=comment]').value = '';"
, "});" , "});"
, "})();" , "})();"
] ]
@@ -848,21 +888,8 @@ recipePage info entries =
-- | Render the cook log section in the marginalia column. -- | Render the cook log section in the marginalia column.
renderCookLog :: FilePath -> [CookLog.CookEntry] -> Html renderCookLog :: FilePath -> [CookLog.CookEntry] -> Html
renderCookLog _ [] = pure () renderCookLog _ [] = pure ()
renderCookLog filename entries = do renderCookLog _filename entries = do
H.h2 "Cook history" H.h2 "Cook history"
H.form
! A.class_ "roux-cook-log-form"
! H.dataAttribute "filename" (H.toValue (T.pack filename))
! A.style "display: flex; gap: 0.3rem; margin-bottom: 0.5rem;"
$ do
H.input ! A.type_ "date" ! A.name "cooked-date"
H.input
! A.type_ "text"
! A.name "comment"
! A.placeholder "Optional note..."
! A.maxlength "500"
! A.style "flex: 1;"
H.button ! A.type_ "submit" $ "Log it"
H.div ! A.class_ "roux-cook-entries" $ do H.div ! A.class_ "roux-cook-entries" $ do
mapM_ renderEntry entries mapM_ renderEntry entries
@@ -894,10 +921,16 @@ renderRecipe filename recipe entries = do
Nothing -> pure () Nothing -> pure ()
H.div ! A.style "display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;" $ do H.div ! A.style "display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;" $ do
H.h1 ! A.class_ "roux-recipe-title" $ H.toHtml (titleText recipe) H.h1 ! A.class_ "roux-recipe-title" $ H.toHtml (titleText recipe)
case metaSource meta of H.div ! A.style "display: flex; gap: 0.75rem; align-items: center; flex-shrink: 0;" $ do
Just url -> H.a ! A.class_ "roux-source-link" ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" $ do case metaSource meta of
"↗ Original" Just url -> H.a ! A.class_ "roux-source-link" ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" $ do
Nothing -> pure () "↗ Original"
Nothing -> pure ()
H.button
! A.class_ "roux-record-btn"
! A.id "roux-record-btn"
! H.dataAttribute "filename" (H.toValue (T.pack filename))
$ "+ Record meal"
H.div ! A.class_ "roux-desc-row" $ do H.div ! A.class_ "roux-desc-row" $ do
case desc of case desc of
Just d -> H.p ! A.class_ "roux-desc" $ H.toHtml d Just d -> H.p ! A.class_ "roux-desc" $ H.toHtml d
@@ -940,6 +973,19 @@ renderRecipe filename recipe entries = do
unless (null notes) $ do unless (null notes) $ do
H.h2 "Notes" H.h2 "Notes"
mapM_ renderMarginalNote notes mapM_ renderMarginalNote notes
-- Record meal modal
H.div ! A.id "roux-record-modal" ! A.style "display: none;" $ do
H.div ! A.class_ "roux-modal-backdrop" ! A.id "roux-modal-backdrop" $ ""
H.div ! A.class_ "roux-modal-dialog" $ do
H.h3 "Record meal"
H.form ! A.id "roux-record-form" $ do
H.label ! A.for "roux-record-date" $ "Date"
H.input ! A.type_ "date" ! A.id "roux-record-date" ! A.name "cooked-date" ! A.required ""
H.label ! A.for "roux-record-comment" $ "Comment (optional)"
H.textarea ! A.id "roux-record-comment" ! A.name "comment" ! A.placeholder "How did it go?" ! A.maxlength "500" $ ""
H.div ! A.class_ "roux-modal-actions" $ do
H.button ! A.type_ "button" ! A.class_ "roux-modal-cancel" ! A.id "roux-modal-cancel" $ "Cancel"
H.button ! A.type_ "submit" ! A.class_ "roux-modal-confirm" $ "Log it"
-- | Extract display title from recipe metadata or fallback. -- | Extract display title from recipe metadata or fallback.
titleText :: Recipe -> Text titleText :: Recipe -> Text