Version 8 #43

Merged
danieljsummers merged 37 commits from version-8 into main 2022-08-19 19:08:31 +00:00
5 changed files with 328 additions and 310 deletions
Showing only changes of commit 810b5d8258 - Show all commits

View File

@ -6,7 +6,7 @@ open PrayerTracker.ViewModels
/// View for the church edit page /// View for the church edit page
let edit (m : EditChurch) ctx vi = let edit (m : EditChurch) ctx vi =
let pageTitle = match m.IsNew with true -> "Add a New Church" | false -> "Edit Church" let pageTitle = if m.IsNew then "Add a New Church" else "Edit Church"
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
[ form [ _action "/church/save"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/church/save"; _method "post"; _class "pt-center-columns" ] [
style [ _scoped ] [ style [ _scoped ] [

View File

@ -27,7 +27,7 @@ let space = rawText " "
let icon name = i [ _class "material-icons" ] [ rawText name ] let icon name = i [ _class "material-icons" ] [ rawText name ]
/// Generate a Material Design icon, specifying the point size (must be defined in CSS) /// Generate a Material Design icon, specifying the point size (must be defined in CSS)
let iconSized size name = i [ _class $"material-icons md-{size}" ] [ rawText name ] let iconSized size name = i [ _class $"material-icons md-%i{size}" ] [ rawText name ]
/// Generate a CSRF prevention token /// Generate a CSRF prevention token
let csrfToken (ctx : HttpContext) = let csrfToken (ctx : HttpContext) =
@ -80,13 +80,11 @@ let namedColorList name selected attrs (s : IStringLocalizer) =
/// Generate an input[type=radio] that is selected if its value is the current value /// Generate an input[type=radio] that is selected if its value is the current value
let radio name domId value current = let radio name domId value current =
input input [ _type "radio"
[ _type "radio"
_name name _name name
_id domId _id domId
_value value _value value
if value = current then _checked if value = current then _checked ]
]
/// Generate a select list with the current value selected /// Generate a select list with the current value selected
let selectList name selected attrs items = let selectList name selected attrs items =
@ -100,7 +98,7 @@ let selectList name selected attrs items =
|> select (List.concat [ [ _name name; _id name ]; attrs ]) |> select (List.concat [ [ _name name; _id name ]; attrs ])
/// Generate the text for a default entry at the top of a select list /// Generate the text for a default entry at the top of a select list
let selectDefault text = $"— {text} " let selectDefault text = $"— %s{text} "
/// Generate a standard submit button with icon and text /// Generate a standard submit button with icon and text
let submit attrs ico text = button (_type "submit" :: attrs) [ icon ico; rawText "  "; locStr text ] let submit attrs ico text = button (_type "submit" :: attrs) [ icon ico; rawText "  "; locStr text ]
@ -108,29 +106,13 @@ let submit attrs ico text = button (_type "submit" :: attrs) [ icon ico; rawText
open System open System
// TODO: this is where to implement issue #1
/// Format a GUID with no dashes (used for URLs and forms) /// Format a GUID with no dashes (used for URLs and forms)
let flatGuid (x : Guid) = x.ToString "N" let flatGuid (x : Guid) = x.ToString "N"
/// An empty GUID string (used for "add" actions) /// An empty GUID string (used for "add" actions)
let emptyGuid = flatGuid Guid.Empty let emptyGuid = flatGuid Guid.Empty
/// blockquote tag
let blockquote = tag "blockquote"
/// role attribute
let _role = attr "role"
/// aria-* attribute
let _aria typ = attr $"aria-{typ}"
/// onclick attribute
let _onclick = attr "onclick"
/// onsubmit attribute
let _onsubmit = attr "onsubmit"
/// scoped flag (used for <style> tag)
let _scoped = flag "scoped"
/// The name this function used to have when the view engine was part of Giraffe /// The name this function used to have when the view engine was part of Giraffe
let renderHtmlNode = RenderView.AsString.htmlNode let renderHtmlNode = RenderView.AsString.htmlNode

View File

@ -2,6 +2,8 @@
module PrayerTracker.Views.Layout module PrayerTracker.Views.Layout
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open PrayerTracker open PrayerTracker
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
open System open System
@ -12,6 +14,16 @@ open System.Globalization
let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" else "en" let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" else "en"
/// Known htmx targets
module Target =
/// htmx links target the body element
let body = _hxTarget "body"
/// htmx links target the #pt-body element
let content = _hxTarget "#pt-body"
/// Navigation items /// Navigation items
module Navigation = module Navigation =
@ -23,39 +35,46 @@ module Navigation =
match m.User with match m.User with
| Some u -> | Some u ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn" a [ _class "dropbtn"; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [
_role "button" icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down"
_aria "label" s["Requests"].Value ]
_title s["Requests"].Value ] div [ _class "dropdown-content"; _roleMenuBar ] [
[ icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down" ] a [ _href "/prayer-requests"; _roleMenuItem ] [
div [ _class "dropdown-content"; _role "menu" ] [ icon "compare_arrows"; menuSpacer; locStr s["Maintain"]
a [ _href "/prayer-requests" ] [ icon "compare_arrows"; menuSpacer; locStr s["Maintain"] ] ]
a [ _href "/prayer-requests/view" ] [ icon "list"; menuSpacer; locStr s["View List"] ] a [ _href "/prayer-requests/view"; _roleMenuItem ] [
icon "list"; menuSpacer; locStr s["View List"]
]
] ]
] ]
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _role "button"; _aria "label" s["Group"].Value; _title s["Group"].Value ] a [ _class "dropbtn"; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [
[ icon "group"; space; locStr s["Group"]; space; icon "keyboard_arrow_down" ] icon "group"; space; locStr s["Group"]; space; icon "keyboard_arrow_down"
div [ _class "dropdown-content"; _role "menu" ] [ ]
a [ _href "/small-group/members" ] div [ _class "dropdown-content"; _roleMenuBar ] [
[ icon "email"; menuSpacer; locStr s["Maintain Group Members"] ] a [ _href "/small-group/members"; _roleMenuItem ] [
a [ _href "/small-group/announcement" ] icon "email"; menuSpacer; locStr s["Maintain Group Members"]
[ icon "send"; menuSpacer; locStr s["Send Announcement"] ] ]
a [ _href "/small-group/preferences" ] a [ _href "/small-group/announcement"; _roleMenuItem ] [
[ icon "build"; menuSpacer; locStr s["Change Preferences"] ] icon "send"; menuSpacer; locStr s["Send Announcement"]
]
a [ _href "/small-group/preferences"; _roleMenuItem ] [
icon "build"; menuSpacer; locStr s["Change Preferences"]
]
] ]
] ]
if u.isAdmin then if u.isAdmin then
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn" a [ _class "dropbtn"
_role "button" _ariaLabel s["Administration"].Value
_aria "label" s["Administration"].Value
_title s["Administration"].Value _title s["Administration"].Value
] [ icon "settings"; space; locStr s["Administration"]; space; icon "keyboard_arrow_down" ] _roleButton ] [
div [ _class "dropdown-content"; _role "menu" ] [ icon "settings"; space; locStr s["Administration"]; space; icon "keyboard_arrow_down"
a [ _href "/churches" ] [ icon "home"; menuSpacer; locStr s["Churches"] ] ]
a [ _href "/small-groups" ] [ icon "send"; menuSpacer; locStr s["Groups"] ] div [ _class "dropdown-content"; _roleMenuBar ] [
a [ _href "/users" ] [ icon "build"; menuSpacer; locStr s["Users"] ] a [ _href "/churches"; _roleMenuItem ] [ icon "home"; menuSpacer; locStr s["Churches"] ]
a [ _href "/small-groups"; _roleMenuItem ] [ icon "send"; menuSpacer; locStr s["Groups"] ]
a [ _href "/users"; _roleMenuItem ] [ icon "build"; menuSpacer; locStr s["Users"] ]
] ]
] ]
| None -> | None ->
@ -63,63 +82,72 @@ module Navigation =
| Some _ -> | Some _ ->
li [] [ li [] [
a [ _href "/prayer-requests/view" a [ _href "/prayer-requests/view"
_aria "label" s["View Request List"].Value _ariaLabel s["View Request List"].Value
_title s["View Request List"].Value _title s["View Request List"].Value ] [
] [ icon "list"; space; locStr s["View Request List"] ] icon "list"; space; locStr s["View Request List"]
]
] ]
| None -> | None ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn" a [ _class "dropbtn"; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [
_role "button" icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down"
_aria "label" s["Log On"].Value ]
_title s["Log On"].Value div [ _class "dropdown-content"; _roleMenuBar ] [
] [ icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down" ] a [ _href "/user/log-on"; _roleMenuItem ] [ icon "person"; menuSpacer; locStr s["User"] ]
div [ _class "dropdown-content"; _role "menu" ] [ a [ _href "/small-group/log-on"; _roleMenuItem ] [
a [ _href "/user/log-on" ] [ icon "person"; menuSpacer; locStr s["User"] ] icon "group"; menuSpacer; locStr s["Group"]
a [ _href "/small-group/log-on" ] [ icon "group"; menuSpacer; locStr s["Group"] ] ]
] ]
] ]
li [] [ li [] [
a [ _href "/prayer-requests/lists" a [ _href "/prayer-requests/lists"
_aria "label" s["View Request List"].Value _ariaLabel s["View Request List"].Value
_title s["View Request List"].Value _title s["View Request List"].Value ] [
] [ icon "list"; space; locStr s["View Request List"] ] icon "list"; space; locStr s["View Request List"]
]
] ]
li [] [ li [] [
a [ _href $"https://docs.prayer.bitbadger.solutions/{langCode ()}" a [ _href $"https://docs.prayer.bitbadger.solutions/{langCode ()}"
_aria "label" s["Help"].Value; _ariaLabel s["Help"].Value
_title s["View Help"].Value _title s["View Help"].Value
_target "_blank" _target "_blank"
] [ icon "help"; space; locStr s["Help"] ] _rel "noopener" ] [
icon "help"; space; locStr s["Help"]
]
] ]
] ]
let rightLinks = let rightLinks =
match m.Group with match m.Group with
| Some _ -> [ | Some _ ->
match m.User with [ match m.User with
| Some _ -> | Some _ ->
li [] [ li [] [
a [ _href "/user/password" a [ _href "/user/password"
_aria "label" s["Change Your Password"].Value _ariaLabel s["Change Your Password"].Value
_title s["Change Your Password"].Value _title s["Change Your Password"].Value ] [
] [ icon "lock"; space; locStr s["Change Your Password"] ] icon "lock"; space; locStr s["Change Your Password"]
]
] ]
| None -> () | None -> ()
li [] [ li [] [
a [ _href "/log-off"; _aria "label" s["Log Off"].Value; _title s["Log Off"].Value ] a [ _href "/log-off"
[ icon "power_settings_new"; space; locStr s["Log Off"] ] _ariaLabel s["Log Off"].Value
_title s["Log Off"].Value
_hxTarget "body" ] [
icon "power_settings_new"; space; locStr s["Log Off"]
]
] ]
] ]
| None -> [] | None -> []
header [ _class "pt-title-bar" ] [ header [ _class "pt-title-bar"; Target.content ] [
section [ _class "pt-title-bar-left" ] [ section [ _class "pt-title-bar-left"; _ariaLabel "Left side of top menu" ] [
span [ _class "pt-title-bar-home" ] [ span [ _class "pt-title-bar-home" ] [
a [ _href "/"; _title s["Home"].Value ] [ locStr s["PrayerTracker"] ] a [ _href "/"; _title s["Home"].Value ] [ locStr s["PrayerTracker"] ]
] ]
ul [] leftLinks ul [] leftLinks
] ]
section [ _class "pt-title-bar-center" ] [] section [ _class "pt-title-bar-center"; _ariaLabel "Empty center space in top menu" ] []
section [ _class "pt-title-bar-right"; _role "toolbar" ] [ section [ _class "pt-title-bar-right"; _roleToolBar; _ariaLabel "Right side of top menu" ] [
ul [] rightLinks ul [] rightLinks
] ]
] ]
@ -127,7 +155,7 @@ module Navigation =
/// Identity bar (below top nav) /// Identity bar (below top nav)
let identity m = let identity m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
header [ _id "pt-language" ] [ header [ _id "pt-language"; Target.body ] [
div [] [ div [] [
span [ _class "u" ] [ locStr s["Language"]; rawText ": " ] span [ _class "u" ] [ locStr s["Language"]; rawText ": " ]
match langCode () with match langCode () with
@ -141,8 +169,8 @@ module Navigation =
a [ _href "/language/es" ] [ locStr s["Cambie a Español"] ] a [ _href "/language/es" ] [ locStr s["Cambie a Español"] ]
] ]
match m.Group with match m.Group with
| Some g ->[ | Some g ->
match m.User with [ match m.User with
| Some u -> | Some u ->
span [ _class "u" ] [ locStr s["Currently Logged On"] ] span [ _class "u" ] [ locStr s["Currently Logged On"] ]
rawText "&nbsp; &nbsp;" rawText "&nbsp; &nbsp;"
@ -155,7 +183,7 @@ module Navigation =
icon "group" icon "group"
space space
match m.User with match m.User with
| Some _ -> a [ _href "/small-group" ] [ strong [] [ str g.name ] ] | Some _ -> a [ _href "/small-group"; Target.content ] [ strong [] [ str g.name ] ]
| None -> strong [] [ str g.name ] | None -> strong [] [ str g.name ]
rawText " &nbsp;" rawText " &nbsp;"
] ]
@ -178,12 +206,11 @@ module Content =
let private titleSep = rawText " &#xab; " let private titleSep = rawText " &#xab; "
/// Common HTML head tag items /// Common HTML head tag items
let private commonHead = let private commonHead = [
[ meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
meta [ _name "generator"; _content "Giraffe" ] meta [ _name "generator"; _content "Giraffe" ]
link [ _rel "stylesheet"; _href "https://fonts.googleapis.com/icon?family=Material+Icons" ] link [ _rel "stylesheet"; _href "https://fonts.googleapis.com/icon?family=Material+Icons" ]
link [ _rel "stylesheet"; _href "/css/app.css" ] link [ _rel "stylesheet"; _href "/css/app.css" ]
script [ _src "/js/app.js" ] []
] ]
/// Render the <head> portion of the page /// Render the <head> portion of the page
@ -203,8 +230,12 @@ let private htmlHead m pageTitle =
let private helpLink link = let private helpLink link =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
sup [] [ sup [] [
a [ _href link; _title s["Click for Help on This Page"].Value; _onclick $"return PT.showHelp('{link}')" ] a [ _href link
[ icon "help_outline" ] _title s["Click for Help on This Page"].Value
_onclick $"return PT.showHelp('{link}')"
_hxNoBoost ] [
icon "help_outline"
]
] ]
/// Render the page title, and optionally a help link /// Render the page title, and optionally a help link
@ -240,7 +271,7 @@ let private messages m =
/// Render the <footer> at the bottom of the page /// Render the <footer> at the bottom of the page
let private htmlFooter m = let private htmlFooter m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let imgText = sprintf "%O %O" s["PrayerTracker"] s["from Bit Badger Solutions"] let imgText = $"""%O{s["PrayerTracker"]} %O{s["from Bit Badger Solutions"]}"""
let resultTime = TimeSpan(DateTime.Now.Ticks - m.RequestStart).TotalSeconds let resultTime = TimeSpan(DateTime.Now.Ticks - m.RequestStart).TotalSeconds
footer [] [ footer [] [
div [ _id "pt-legal" ] [ div [ _id "pt-legal" ] [
@ -251,27 +282,31 @@ let private htmlFooter m =
a [ _href "https://github.com/bit-badger/PrayerTracker" a [ _href "https://github.com/bit-badger/PrayerTracker"
_title s["View source code and get technical support"].Value _title s["View source code and get technical support"].Value
_target "_blank" _target "_blank"
_rel "noopener" _rel "noopener" ] [
] [ locStr s["Source & Support"] locStr s["Source & Support"]
] ]
] ]
div [ _id "pt-footer" ] [ div [ _id "pt-footer" ] [
a [ _href "/"; _style "line-height:28px;" ] a [ _href "/"; _style "line-height:28px;" ] [
[ img [ _src $"""/img/%O{s["footer_en"]}.png"""; _alt imgText; _title imgText ] ] img [ _src $"""/img/%O{s["footer_en"]}.png"""; _alt imgText; _title imgText ]
]
str m.Version str m.Version
space space
i [ _title s["This page loaded in {0:N3} seconds", resultTime].Value; _class "material-icons md-18" ] i [ _title s["This page loaded in {0:N3} seconds", resultTime].Value; _class "material-icons md-18" ] [
[ str "schedule" ] str "schedule"
] ]
] ]
Script.minified
script [ _src "/js/app.js" ] []
]
/// The standard layout for PrayerTracker /// The standard layout for PrayerTracker
let standard m pageTitle (content : XmlNode) = let standard m pageTitle (content : XmlNode) =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let ttl = s[pageTitle] let ttl = s[pageTitle]
html [ _lang "" ] [ html [ _lang (langCode ()) ] [
htmlHead m ttl htmlHead m ttl
body [] [ body [ _hxBoost ] [
Navigation.top m Navigation.top m
div [ _id "pt-body" ] [ div [ _id "pt-body" ] [
Navigation.identity m Navigation.identity m
@ -287,7 +322,7 @@ let standard m pageTitle (content : XmlNode) =
let bare pageTitle content = let bare pageTitle content =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let ttl = s[pageTitle] let ttl = s[pageTitle]
html [ _lang "" ] [ html [ _lang (langCode ()) ] [
head [] [ head [] [
meta [ _charset "UTF-8" ] meta [ _charset "UTF-8" ]
title [] [ locStr ttl; titleSep; locStr s["PrayerTracker"] ] title [] [ locStr ttl; titleSep; locStr s["PrayerTracker"] ]

View File

@ -20,6 +20,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.0" />
<PackageReference Include="MailKit" Version="3.3.0" /> <PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />