Version 3 #67

Merged
danieljsummers merged 53 commits from version-3 into master 2021-10-26 23:39:59 +00:00
3 changed files with 77 additions and 60 deletions
Showing only changes of commit 6023db6e55 - Show all commits

View File

@ -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
]

View File

@ -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 " &#xab; 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 " &#xab; 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 ]
]
]
]

View File

@ -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 += " &#xab; 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"))