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

View File

@ -5,13 +5,31 @@ open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx open Giraffe.ViewEngine.Htmx
open System 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>] [<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 = 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 " &#xab; 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 " &#xab; 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 ]
]
] ]
]

View File

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