feat: replace inline cook log form with 'Record meal' button + modal
Build and Deploy / build-and-deploy (push) Successful in 17m20s
Build and Deploy / build-and-deploy (push) Successful in 17m20s
This commit is contained in:
+71
-25
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user