-- | The TodoMVC page, rendered on the server. It follows TodoMVC's own -- template, plus the plain forms that make every action work without -- JavaScript. @static/app.css@ lays those forms out and @static/app.js@ -- takes over from them when it runs. module Todo.Html ( View (..) , page , fragment ) where import Control.Monad (forM_, unless) import Data.Text (Text) import Data.Text qualified as T import Lucid import Lucid.Base (makeAttributes) import Todo.Model -- | Everything one render needs. data View = View { viewFilter :: Filter , viewTodos :: [Todo] -- ^ The whole list, whatever the filter. , viewEditing :: Maybe TodoId , viewAssetVersion :: Text -- ^ Appended to this app's own asset URLs, so a new build is fetched. } page :: View -> Html () page v = doctype_ >> html_ [lang_ "en"] (do head_ $ do meta_ [charset_ "utf-8"] meta_ [name_ "viewport", content_ "width=device-width, initial-scale=1"] title_ "TodoMVC" link_ [rel_ "stylesheet", href_ "/static/base.css"] link_ [rel_ "stylesheet", href_ "/static/index.css"] link_ [rel_ "stylesheet", href_ ("/static/app.css?v=" <> viewAssetVersion v)] script_ [type_ "module", src_ ("/static/app.js?v=" <> viewAssetVersion v)] ("" :: Text) body_ $ do section_ [class_ "todoapp"] $ do header_ [class_ "header"] $ do h1_ "todos" postForm v "/todos" $ input_ $ [class_ "new-todo", name_ "title", placeholder_ "What needs to be done?", autocomplete_ "off"] <> [autofocus_ | null (viewEditing v)] fragment v footer_ [class_ "info"] $ do p_ "Double-click to edit a todo" p_ $ do "Created by " a_ [href_ "https://bakeoff.bfj.oncarlos.com"] "the CARLOS bakeoff" " ยท Haskell" p_ $ do "Part of " a_ [href_ "http://todomvc.com"] "TodoMVC") -- | @section.main@ and @footer.footer@: the part of the page every -- action changes. With JavaScript on, actions answer with just this. fragment :: View -> Html () fragment v = do section_ ([class_ "main"] <> hiddenIf (null todos)) $ do input_ ([id_ "toggle-all", class_ "toggle-all", type_ "checkbox"] <> [checked_ | allDone]) label_ [for_ "toggle-all"] "Mark all as complete" postForm v "/todos/toggle-all" $ button_ [class_ "toggle-all-submit", data_ "action" "toggle-all"] "Mark all as complete" ul_ [class_ "todo-list"] $ forM_ (filter (visible (viewFilter v)) todos) (item v) footer_ ([class_ "footer"] <> hiddenIf (null todos)) $ do span_ [class_ "todo-count"] $ do strong_ (toHtml (show left)) if left == 1 then " item left" else " items left" ul_ [class_ "filters"] $ forM_ [minBound .. maxBound] $ \f -> do unless (f == minBound) " " li_ $ a_ ([href_ (filterPath f)] <> [class_ "selected" | f == viewFilter v]) (filterLabel f) postForm v "/todos/clear-completed" $ button_ ([class_ "clear-completed"] <> hiddenIf (left == length todos)) "Clear completed" where todos = viewTodos v left = length (filter (not . todoCompleted) todos) allDone = not (null todos) && left == 0 item :: View -> Todo -> Html () item v t = li_ ([data_ "id" tid] <> [class_ (T.unwords classes) | not (null classes)]) $ do div_ [class_ "view"] $ do input_ ([class_ "toggle", type_ "checkbox"] <> [checked_ | todoCompleted t]) label_ $ a_ [data_ "action" "edit", href_ editHref] (toHtml (todoTitle t)) postForm v (base <> "/toggle") $ button_ [ class_ "toggle-submit" , data_ "action" "toggle" , makeAttributes "aria-label" (if todoCompleted t then "Mark as active" else "Mark as complete") ] "" postForm v (base <> "/destroy") $ button_ [class_ "destroy", makeAttributes "aria-label" "Delete"] "" postForm v (base <> "/edit") $ input_ ([class_ "edit", name_ "title", value_ (todoTitle t)] <> [autofocus_ | editing]) where tid = todoIdText (todoId t) base = "/todos/" <> tid editing = viewEditing v == Just (todoId t) classes = ["completed" | todoCompleted t] <> ["editing" | editing] editHref = filterPath (viewFilter v) <> "?edit=" <> tid -- | A form that posts to one action, carrying the current filter so the -- redirect comes back to it. Laid out with @display: contents@, so it -- doesn't disturb TodoMVC's CSS. postForm :: View -> Text -> Html () -> Html () postForm v action body = form_ [method_ "post", action_ action, class_ "action"] $ do input_ [type_ "hidden", name_ "filter", value_ (filterName (viewFilter v))] body hiddenIf :: Bool -> [Attributes] hiddenIf b = [hidden_ "" | b] filterLabel :: Filter -> Html () filterLabel = \case All -> "All" Active -> "Active" Completed -> "Completed"