Version 3 #67
@ -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
|
||||
]
|
||||
|
@ -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
|
||||
[<AutoOpen>]
|
||||
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 ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -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"))
|
||||
|
Loading…
Reference in New Issue
Block a user