diff --git a/src/MyPrayerJournal/Server/Handlers.fs b/src/MyPrayerJournal/Server/Handlers.fs index e200b71..cf44871 100644 --- a/src/MyPrayerJournal/Server/Handlers.fs +++ b/src/MyPrayerJournal/Server/Handlers.fs @@ -98,6 +98,15 @@ module private Helpers = return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes) } + /// Create a page rendering context + let pageContext (ctx : HttpContext) pageTitle content : Views.PageRenderContext = + { isAuthenticated = (user >> Option.isSome) ctx + hasSnoozed = false + currentUrl = ctx.Request.Path.Value + pageTitle = pageTitle + content = content + } + /// Composable handler to write a view to the output let writeView view : HttpHandler = fun next ctx -> task { @@ -107,12 +116,12 @@ module private Helpers = /// Send a partial result if this is not a full page load let partialIfNotRefresh (pageTitle : string) content : HttpHandler = fun next ctx -> - (next, ctx) - ||> match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with - | true -> - ctx.Response.Headers.["X-Page-Title"] <- Microsoft.Extensions.Primitives.StringValues pageTitle - withHxTriggerAfterSettle "menu-refresh" >=> writeView content - | false -> writeView (Views.Layout.view pageTitle content) + let view = + pageContext ctx pageTitle content + |> match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with + | true -> Views.Layout.partial + | false -> Views.Layout.view + writeView view next ctx /// Add a success message header to the response let withSuccessMessage : string -> HttpHandler = @@ -159,14 +168,6 @@ open MyPrayerJournal.Data.Extensions /// Handlers for less-than-full-page HTML requests module Components = - // GET /components/nav-items - let navItems : HttpHandler = - fun next ctx -> task { - let url = ctx.Request.Headers.HxCurrentUrl - let isAuthorized = ctx |> (user >> Option.isSome) - return! renderComponent (Views.Navigation.currentNav isAuthorized false url) next ctx - } - // GET /components/journal-items let journalItems : HttpHandler = requiresAuthentication Error.notAuthorized @@ -470,7 +471,6 @@ let routes = subRoute "/components/" [ GET_HEAD [ route "journal-items" Components.journalItems - route "nav-items" Components.navItems routef "request/%s/edit" Components.requestEdit routef "request/%s/item" Components.requestItem ] diff --git a/src/MyPrayerJournal/Server/Views.fs b/src/MyPrayerJournal/Server/Views.fs index 34fe3ff..36a54ba 100644 --- a/src/MyPrayerJournal/Server/Views.fs +++ b/src/MyPrayerJournal/Server/Views.fs @@ -5,13 +5,31 @@ open Giraffe.ViewEngine.Accessibility open Giraffe.ViewEngine.Htmx open System +// fsharplint:disable RecordFieldNames + +/// The data needed to render a page-level view +type PageRenderContext = + { /// Whether the user is authenticated + isAuthenticated : bool + /// Whether the user has snoozed requests + hasSnoozed : bool + /// The current URL + currentUrl : string + /// The title for the page to be rendered + pageTitle : string + /// The content of the page + content : XmlNode + } + + +/// Internal partial views [] -module Helpers = +module private Helpers = - /// Create a link that targets the `main` element and pushes a URL to history + /// Create a link that targets the `#top` element and pushes a URL to history let pageLink href attrs = attrs - |> List.append [ _href href; _hxBoost; _hxTarget "main"; _hxPushUrl ] + |> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxPushUrl ] |> a /// Create a Material icon @@ -218,42 +236,33 @@ module Legal = module Navigation = /// The default navigation bar, which will load the items on page load, and whenever a refresh event occurs - let navBar = - nav [ _class "navbar navbar-dark" ] [ + let navBar ctx = + nav [ _class "navbar navbar-dark"; _roleNavigation ] [ div [ _class "container-fluid" ] [ pageLink "/" [ _class "navbar-brand" ] [ span [ _class "m" ] [ str "my" ] span [ _class "p" ] [ str "Prayer" ] span [ _class "j" ] [ str "Journal" ] ] - ul [ - _class "navbar-nav me-auto d-flex flex-row" - _hxGet "/components/nav-items" - _hxTarget ".navbar-nav" - _hxTrigger (sprintf "%s, menu-refresh from:body" HxTrigger.Load) - ] [ ] + seq { + let navLink (matchUrl : string) = + match ctx.currentUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> [] + |> pageLink matchUrl + match ctx.isAuthenticated with + | true -> + li [ _class "nav-item" ] [ navLink "/journal" [ str "Journal" ] ] + li [ _class "nav-item" ] [ navLink "/requests/active" [ str "Active" ] ] + if ctx.hasSnoozed then li [ _class "nav-item" ] [ navLink "/requests/snoozed" [ str "Snoozed" ] ] + li [ _class "nav-item" ] [ navLink "/requests/answered" [ str "Answered" ] ] + li [ _class "nav-item" ] [ a [ _href "/user/log-off" ] [ str "Log Off" ] ] + | false -> li [ _class "nav-item"] [ a [ _href "/user/log-on" ] [ str "Log On" ] ] + li [ _class "nav-item" ] [ a [ _href "https://docs.prayerjournal.me"; _target "_blank" ] [ str "Docs" ] ] + } + |> List.ofSeq + |> ul [ _class "navbar-nav me-auto d-flex flex-row" ] ] ] - /// Generate the navigation items based on the current state - let currentNav isAuthenticated hasSnoozed (url : Uri option) = - seq { - let currUrl = match url with Some u -> (u.PathAndQuery.Split '?').[0] | None -> "" - let navLink (matchUrl : string) = - match currUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> [] - |> pageLink matchUrl - match isAuthenticated with - | true -> - li [ _class "nav-item" ] [ navLink "/journal" [ str "Journal" ] ] - li [ _class "nav-item" ] [ navLink "/requests/active" [ str "Active" ] ] - if hasSnoozed then li [ _class "nav-item" ] [ navLink "/requests/snoozed" [ str "Snoozed" ] ] - li [ _class "nav-item" ] [ navLink "/requests/answered" [ str "Answered" ] ] - li [ _class "nav-item" ] [ a [ _href "/user/log-off" ] [ str "Log Off" ] ] - | false -> li [ _class "nav-item"] [ a [ _href "/user/log-on" ] [ str "Log On" ] ] - li [ _class "nav-item" ] [ a [ _href "https://docs.prayerjournal.me"; _target "_blank" ] [ str "Docs" ] ] - } - |> List.ofSeq - /// Views for journal pages and components module Journal = @@ -578,11 +587,14 @@ module Request = /// Layout views module Layout = + /// The title tag with the application name appended + let titleTag ctx = title [] [ str ctx.pageTitle; rawText " « myPrayerJournal" ] + /// The HTML `head` element - let htmlHead pageTitle = + let htmlHead ctx = head [ _lang "en" ] [ meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] - title [] [ str pageTitle; rawText " « myPrayerJournal" ] + titleTag ctx link [ _href "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" _rel "stylesheet" @@ -632,14 +644,25 @@ module Layout = ] /// Create the full view of the page - let view pageTitle content = + let view ctx = html [ _lang "en" ] [ - htmlHead pageTitle - body [ _hxHeaders "" ] [ - Navigation.navBar - main [] [ content ] + htmlHead ctx + body [] [ + section [ _id "top" ] [ + Navigation.navBar ctx + main [ _roleMain ] [ ctx.content ] + ] toaster htmlFoot + ] + ] + + /// Create a partial view + let partial ctx = + html [ _lang "en" ] [ + head [] [ titleTag ctx ] + body [] [ + Navigation.navBar ctx + main [ _roleMain ] [ ctx.content ] + ] ] - ] - diff --git a/src/MyPrayerJournal/Server/wwwroot/script/mpj.js b/src/MyPrayerJournal/Server/wwwroot/script/mpj.js index 51b1bd3..56a67ee 100644 --- a/src/MyPrayerJournal/Server/wwwroot/script/mpj.js +++ b/src/MyPrayerJournal/Server/wwwroot/script/mpj.js @@ -56,12 +56,6 @@ const mpj = { htmx.on("htmx:afterOnLoad", function (evt) { const hdrs = evt.detail.xhr.getAllResponseHeaders() - // Set the page title if a header was in the response - if (hdrs.indexOf("x-page-title") >= 0) { - const title = document.querySelector("title") - title.innerText = evt.detail.xhr.getResponseHeader("x-page-title") - title.innerHTML += " « myPrayerJournal" - } // Show a message if there was one in the response if (hdrs.indexOf("x-toast") >= 0) { mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))