From 930fba369cb46f42a14dd64a0eaa14273d5e95c8 Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Mon, 25 May 2026 22:20:00 -0400 Subject: [PATCH] feat: replace inline cook log form with 'Record meal' button + modal --- src/Roux/Html.hs | 96 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 25 deletions(-) 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