diff --git a/src/Roux/Html.hs b/src/Roux/Html.hs
index 1b8765b..c244d78 100644
--- a/src/Roux/Html.hs
+++ b/src/Roux/Html.hs
@@ -229,6 +229,22 @@ page title content =
, ".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-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-row { display: flex; gap: 2rem; align-items: flex-start; margin-bottom: 1.5rem; }"
, ".roux-desc-row .roux-desc { flex: 1; }"
@@ -635,14 +651,38 @@ cookLogJs =
T.unlines
[ "(function(){"
, "'use strict';"
- , "var form = document.querySelector('.roux-cook-log-form');"
- , "if (!form) return;"
- , "var dateInput = form.querySelector('[name=cooked-date]');"
- , "if (dateInput && !dateInput.value) dateInput.value = new Date().toISOString().slice(0,10);"
+ , "var btn = document.getElementById('roux-record-btn');"
+ , "var modal = document.getElementById('roux-record-modal');"
+ , "var backdrop = document.getElementById('roux-modal-backdrop');"
+ , "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) {"
, " e.preventDefault();"
+ , " var filename = btn.dataset.filename;"
, " var fd = new FormData(form);"
- , " var filename = form.dataset.filename;"
, " var res = await fetch('/cook-log/' + encodeURIComponent(filename), {"
, " method: 'POST',"
, " body: fd"
@@ -652,6 +692,7 @@ cookLogJs =
, " alert(err.error || 'Failed to log cooking');"
, " return;"
, " }"
+ , " hideModal();"
, " var date = fd.get('cooked-date');"
, " var comment = fd.get('comment');"
, " var entryDiv = document.createElement('div');"
@@ -670,9 +711,8 @@ cookLogJs =
, " commentSpan.textContent = comment;"
, " entryDiv.appendChild(commentSpan);"
, " }"
- , " var container = form.nextElementSibling;"
+ , " var container = document.querySelector('.roux-cook-entries');"
, " 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.
renderCookLog :: FilePath -> [CookLog.CookEntry] -> Html
renderCookLog _ [] = pure ()
-renderCookLog filename entries = do
+renderCookLog _filename entries = do
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
mapM_ renderEntry entries
@@ -894,10 +921,16 @@ renderRecipe filename recipe entries = do
Nothing -> pure ()
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)
- case metaSource meta of
- Just url -> H.a ! A.class_ "roux-source-link" ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" $ do
- "↗ Original"
- Nothing -> pure ()
+ H.div ! A.style "display: flex; gap: 0.75rem; align-items: center; flex-shrink: 0;" $ do
+ case metaSource meta of
+ Just url -> H.a ! A.class_ "roux-source-link" ! A.href (H.toValue url) ! A.target "_blank" ! A.rel "noopener noreferrer" $ do
+ "↗ 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
case desc of
Just d -> H.p ! A.class_ "roux-desc" $ H.toHtml d
@@ -940,6 +973,19 @@ renderRecipe filename recipe entries = do
unless (null notes) $ do
H.h2 "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.
titleText :: Recipe -> Text