Version 3 #67
@ -98,6 +98,15 @@ module private Helpers =
|
|||||||
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
|
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
|
/// Composable handler to write a view to the output
|
||||||
let writeView view : HttpHandler =
|
let writeView view : HttpHandler =
|
||||||
fun next ctx -> task {
|
fun next ctx -> task {
|
||||||
@ -107,12 +116,12 @@ module private Helpers =
|
|||||||
/// Send a partial result if this is not a full page load
|
/// Send a partial result if this is not a full page load
|
||||||
let partialIfNotRefresh (pageTitle : string) content : HttpHandler =
|
let partialIfNotRefresh (pageTitle : string) content : HttpHandler =
|
||||||
fun next ctx ->
|
fun next ctx ->
|
||||||
(next, ctx)
|
let view =
|
||||||
||> match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with
|
pageContext ctx pageTitle content
|
||||||
| true ->
|
|> match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with
|
||||||
ctx.Response.Headers.["X-Page-Title"] <- Microsoft.Extensions.Primitives.StringValues pageTitle
|
| true -> Views.Layout.partial
|
||||||
withHxTriggerAfterSettle "menu-refresh" >=> writeView content
|
| false -> Views.Layout.view
|
||||||
| false -> writeView (Views.Layout.view pageTitle content)
|
writeView view next ctx
|
||||||
|
|
||||||
/// Add a success message header to the response
|
/// Add a success message header to the response
|
||||||
let withSuccessMessage : string -> HttpHandler =
|
let withSuccessMessage : string -> HttpHandler =
|
||||||
@ -159,14 +168,6 @@ open MyPrayerJournal.Data.Extensions
|
|||||||
/// Handlers for less-than-full-page HTML requests
|
/// Handlers for less-than-full-page HTML requests
|
||||||
module Components =
|
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
|
// GET /components/journal-items
|
||||||
let journalItems : HttpHandler =
|
let journalItems : HttpHandler =
|
||||||
requiresAuthentication Error.notAuthorized
|
requiresAuthentication Error.notAuthorized
|
||||||
@ -470,7 +471,6 @@ let routes =
|
|||||||
subRoute "/components/" [
|
subRoute "/components/" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
route "journal-items" Components.journalItems
|
route "journal-items" Components.journalItems
|
||||||
route "nav-items" Components.navItems
|
|
||||||
routef "request/%s/edit" Components.requestEdit
|
routef "request/%s/edit" Components.requestEdit
|
||||||
routef "request/%s/item" Components.requestItem
|
routef "request/%s/item" Components.requestItem
|
||||||
]
|
]
|
||||||
|
@ -5,13 +5,31 @@ open Giraffe.ViewEngine.Accessibility
|
|||||||
open Giraffe.ViewEngine.Htmx
|
open Giraffe.ViewEngine.Htmx
|
||||||
open System
|
open System
|
||||||
|
|
||||||
[<AutoOpen>]
|
// fsharplint:disable RecordFieldNames
|
||||||
module Helpers =
|
|
||||||
|
|
||||||
/// Create a link that targets the `main` element and pushes a URL to history
|
/// 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 private Helpers =
|
||||||
|
|
||||||
|
/// Create a link that targets the `#top` element and pushes a URL to history
|
||||||
let pageLink href attrs =
|
let pageLink href attrs =
|
||||||
attrs
|
attrs
|
||||||
|> List.append [ _href href; _hxBoost; _hxTarget "main"; _hxPushUrl ]
|
|> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxPushUrl ]
|
||||||
|> a
|
|> a
|
||||||
|
|
||||||
/// Create a Material icon
|
/// Create a Material icon
|
||||||
@ -218,42 +236,33 @@ module Legal =
|
|||||||
module Navigation =
|
module Navigation =
|
||||||
|
|
||||||
/// The default navigation bar, which will load the items on page load, and whenever a refresh event occurs
|
/// The default navigation bar, which will load the items on page load, and whenever a refresh event occurs
|
||||||
let navBar =
|
let navBar ctx =
|
||||||
nav [ _class "navbar navbar-dark" ] [
|
nav [ _class "navbar navbar-dark"; _roleNavigation ] [
|
||||||
div [ _class "container-fluid" ] [
|
div [ _class "container-fluid" ] [
|
||||||
pageLink "/" [ _class "navbar-brand" ] [
|
pageLink "/" [ _class "navbar-brand" ] [
|
||||||
span [ _class "m" ] [ str "my" ]
|
span [ _class "m" ] [ str "my" ]
|
||||||
span [ _class "p" ] [ str "Prayer" ]
|
span [ _class "p" ] [ str "Prayer" ]
|
||||||
span [ _class "j" ] [ str "Journal" ]
|
span [ _class "j" ] [ str "Journal" ]
|
||||||
]
|
]
|
||||||
ul [
|
seq {
|
||||||
_class "navbar-nav me-auto d-flex flex-row"
|
let navLink (matchUrl : string) =
|
||||||
_hxGet "/components/nav-items"
|
match ctx.currentUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> []
|
||||||
_hxTarget ".navbar-nav"
|
|> pageLink matchUrl
|
||||||
_hxTrigger (sprintf "%s, menu-refresh from:body" HxTrigger.Load)
|
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
|
/// Views for journal pages and components
|
||||||
module Journal =
|
module Journal =
|
||||||
@ -578,11 +587,14 @@ module Request =
|
|||||||
/// Layout views
|
/// Layout views
|
||||||
module Layout =
|
module Layout =
|
||||||
|
|
||||||
|
/// The title tag with the application name appended
|
||||||
|
let titleTag ctx = title [] [ str ctx.pageTitle; rawText " « myPrayerJournal" ]
|
||||||
|
|
||||||
/// The HTML `head` element
|
/// The HTML `head` element
|
||||||
let htmlHead pageTitle =
|
let htmlHead ctx =
|
||||||
head [ _lang "en" ] [
|
head [ _lang "en" ] [
|
||||||
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
|
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
|
||||||
title [] [ str pageTitle; rawText " « myPrayerJournal" ]
|
titleTag ctx
|
||||||
link [
|
link [
|
||||||
_href "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
_href "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||||
_rel "stylesheet"
|
_rel "stylesheet"
|
||||||
@ -632,14 +644,25 @@ module Layout =
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// Create the full view of the page
|
/// Create the full view of the page
|
||||||
let view pageTitle content =
|
let view ctx =
|
||||||
html [ _lang "en" ] [
|
html [ _lang "en" ] [
|
||||||
htmlHead pageTitle
|
htmlHead ctx
|
||||||
body [ _hxHeaders "" ] [
|
body [] [
|
||||||
Navigation.navBar
|
section [ _id "top" ] [
|
||||||
main [] [ content ]
|
Navigation.navBar ctx
|
||||||
|
main [ _roleMain ] [ ctx.content ]
|
||||||
|
]
|
||||||
toaster
|
toaster
|
||||||
htmlFoot
|
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) {
|
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||||
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
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
|
// Show a message if there was one in the response
|
||||||
if (hdrs.indexOf("x-toast") >= 0) {
|
if (hdrs.indexOf("x-toast") >= 0) {
|
||||||
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
|
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
|
||||||
|
Loading…
Reference in New Issue
Block a user