Version 8 #43

Merged
danieljsummers merged 37 commits from version-8 into main 2022-08-19 19:08:31 +00:00
15 changed files with 886 additions and 819 deletions
Showing only changes of commit 1547377527 - Show all commits

View File

@ -8,53 +8,53 @@ open PrayerTracker.ViewModels
let edit (m : EditChurch) ctx vi = let edit (m : EditChurch) ctx vi =
let pageTitle = if m.IsNew then "Add a New Church" else "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"; Target.content ] [
style [ _scoped ] [ style [ _scoped ] [
rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }" rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }"
]
csrfToken ctx
input [ _type "hidden"; _name (nameof m.ChurchId); _value (flatGuid m.ChurchId) ]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for "name" ] [ locStr s["Church Name"] ]
input [ _type "text"; _name (nameof m.Name); _id "name"; _required; _autofocus; _value m.Name ]
] ]
div [ _class "pt-field" ] [ csrfToken ctx
label [ _for "City"] [ locStr s["City"] ] input [ _type "hidden"; _name (nameof m.ChurchId); _value (flatGuid m.ChurchId) ]
input [ _type "text"; _name (nameof m.City); _id "city"; _required; _value m.City ] div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for "name" ] [ locStr s["Church Name"] ]
input [ _type "text"; _name (nameof m.Name); _id "name"; _required; _autofocus; _value m.Name ]
]
div [ _class "pt-field" ] [
label [ _for "City"] [ locStr s["City"] ]
input [ _type "text"; _name (nameof m.City); _id "city"; _required; _value m.City ]
]
div [ _class "pt-field" ] [
label [ _for "state" ] [ locStr s["State or Province"] ]
input [ _type "text"
_name (nameof m.State)
_id "state"
_minlength "2"; _maxlength "2"
_value m.State
_required ]
]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field-row" ] [
label [ _for "state" ] [ locStr s["State or Province"] ] div [ _class "pt-checkbox-field" ] [
input [ _type "text" input [ _type "checkbox"
_name (nameof m.State) _name (nameof m.HasInterface)
_id "state" _id "hasInterface"
_required _value "True"
_minlength "2"; _maxlength "2" if defaultArg m.HasInterface false then _checked ]
_value m.State ] label [ _for "hasInterface" ] [ locStr s["Has an interface with Virtual Prayer Room"] ]
]
] ]
] div [ _class "pt-field-row pt-fadeable"; _id "divInterfaceAddress" ] [
div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [
div [ _class "pt-checkbox-field" ] [ label [ _for "interfaceAddress" ] [ locStr s["VPR Interface URL"] ]
input [ _type "checkbox" input [ _type "url"
_name (nameof m.HasInterface) _name (nameof m.InterfaceAddress)
_id "hasInterface" _id "interfaceAddress";
_value "True" _value (defaultArg m.InterfaceAddress "") ]
if defaultArg m.HasInterface false then _checked ] ]
label [ _for "hasInterface" ] [ locStr s["Has an interface with Virtual Prayer Room"] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Church"] ]
] ]
div [ _class "pt-field-row pt-fadeable"; _id "divInterfaceAddress" ] [ script [] [ rawText "PT.onLoad(PT.church.edit.onPageLoad)" ]
div [ _class "pt-field" ] [
label [ _for "interfaceAddress" ] [ locStr s["VPR Interface URL"] ]
input [ _type "url"
_name (nameof m.InterfaceAddress)
_id "interfaceAddress";
_value (defaultArg m.InterfaceAddress "") ]
]
]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Church"] ]
]
script [] [ rawText "PT.onLoad(PT.church.edit.onPageLoad)" ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle
@ -87,12 +87,13 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vi
$"""{s["Church"].Value.ToLower ()} ({ch.name})"""] $"""{s["Church"].Value.ToLower ()} ({ch.name})"""]
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/church/{chId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ] a [ _href $"/church/{chId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ]
a [ _href delAction a [ _href delAction
_title s["Delete This Church"].Value _title s["Delete This Church"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
[ icon "delete_forever" ] icon "delete_forever"
] ]
]
td [] [ str ch.name ] td [] [ str ch.name ]
td [] [ str ch.city; rawText ", "; str ch.st ] td [] [ str ch.city; rawText ", "; str ch.st ]
td [ _class "pt-right-text" ] [ rawText (stats[chId].smallGroups.ToString "N0") ] td [ _class "pt-right-text" ] [ rawText (stats[chId].smallGroups.ToString "N0") ]
@ -102,16 +103,17 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vi
]) ])
|> tbody [] |> tbody []
] ]
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ] a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Church"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Church"]
br [] ]
br [] br []
] br []
tableSummary churches.Length s ]
chTbl tableSummary churches.Length s
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] chTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
|> Layout.Content.wide |> Layout.Content.wide
|> Layout.standard vi "Maintain Churches" |> Layout.standard vi "Maintain Churches"

View File

@ -113,6 +113,12 @@ 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
/// Create an HTML onsubmit event handler
let _onsubmit = attr "onsubmit"
/// A "rel='noopener'" attribute
let _relNoOpener = _rel "noopener"
/// 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
@ -143,3 +149,15 @@ module TimeZones =
let name tzId (s : IStringLocalizer) = let name tzId (s : IStringLocalizer) =
try s[xref[tzId]] try s[xref[tzId]]
with :? KeyNotFoundException -> LocalizedString (tzId, tzId) with :? KeyNotFoundException -> LocalizedString (tzId, tzId)
open Giraffe.ViewEngine.Htmx
/// 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"

View File

@ -50,7 +50,6 @@ let index vi =
let l = I18N.forView "Home/Index" let l = I18N.forView "Home/Index"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ p [] [ [ p [] [
raw l["Welcome to <strong>{0}</strong>!", s["PrayerTracker"]] raw l["Welcome to <strong>{0}</strong>!", s["PrayerTracker"]]
space space
@ -128,7 +127,6 @@ let privacyPolicy vi =
let l = I18N.forView "Home/PrivacyPolicy" let l = I18N.forView "Home/PrivacyPolicy"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ p [ _class "pt-right-text" ] [ small [] [ em [] [ raw l["(as of July 31, 2018)"] ] ] ] [ p [ _class "pt-right-text" ] [ small [] [ em [] [ raw l["(as of July 31, 2018)"] ] ] ]
p [] [ p [] [
raw l["The nature of the service is one where privacy is a must."] raw l["The nature of the service is one where privacy is a must."]
@ -150,7 +148,7 @@ let privacyPolicy vi =
rawText " &ndash; " rawText " &ndash; "
raw l["{0} stores the text of prayer requests.", s["PrayerTracker"]] raw l["{0} stores the text of prayer requests.", s["PrayerTracker"]]
space space
raw l["It also stores names and e-mail addreses of small group members, and plain-text passwords for small groups with password-protected lists."] raw l["It also stores names and e-mail addresses of small group members, and plain-text passwords for small groups with password-protected lists."]
] ]
] ]
h3 [] [ raw l["How Your Data Is Accessed / Secured"] ] h3 [] [ raw l["How Your Data Is Accessed / Secured"] ]

View File

@ -3,39 +3,26 @@ module PrayerTracker.Views.Layout
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open PrayerTracker
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
open System
open System.Globalization open System.Globalization
/// Get the two-character language code for the current request /// Get the two-character language code for the current request
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 =
/// Top navigation bar /// Top navigation bar
let top m = let top m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let menuSpacer = rawText "&nbsp; " let menuSpacer = rawText "&nbsp; "
let _dropdown = _class "dropbtn"
let leftLinks = [ let leftLinks = [
match m.User with match m.User with
| Some u -> | Some u ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [ a [ _dropdown; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [
icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down" icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down"
] ]
div [ _class "dropdown-content"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
@ -48,7 +35,7 @@ module Navigation =
] ]
] ]
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [ a [ _dropdown; _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"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
@ -65,7 +52,7 @@ module Navigation =
] ]
if u.isAdmin then if u.isAdmin then
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn" a [ _dropdown
_ariaLabel s["Administration"].Value _ariaLabel s["Administration"].Value
_title s["Administration"].Value _title s["Administration"].Value
_roleButton ] [ _roleButton ] [
@ -89,7 +76,7 @@ module Navigation =
] ]
| None -> | None ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [ a [ _dropdown; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [
icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down" icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down"
] ]
div [ _class "dropdown-content"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
@ -111,7 +98,7 @@ module Navigation =
_ariaLabel s["Help"].Value _ariaLabel s["Help"].Value
_title s["View Help"].Value _title s["View Help"].Value
_target "_blank" _target "_blank"
_rel "noopener" ] [ _relNoOpener ] [
icon "help"; space; locStr s["Help"] icon "help"; space; locStr s["Help"]
] ]
] ]
@ -130,10 +117,7 @@ module Navigation =
] ]
| None -> () | None -> ()
li [] [ li [] [
a [ _href "/log-off" a [ _href "/log-off"; _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ] [
_ariaLabel s["Log Off"].Value
_title s["Log Off"].Value
_hxTarget "body" ] [
icon "power_settings_new"; space; locStr s["Log Off"] icon "power_settings_new"; space; locStr s["Log Off"]
] ]
] ]
@ -222,10 +206,11 @@ let private htmlHead m pageTitle =
yield! commonHead yield! commonHead
for cssFile in m.Style do for cssFile in m.Style do
link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ] link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ]
for jsFile in m.Script do
script [ _src $"/js/{jsFile}.js" ] []
] ]
open Giraffe.ViewEngine.Htmx
/// Render a link to the help page for the current page /// Render a link to the help page for the current page
let private helpLink link = let private helpLink link =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
@ -241,7 +226,7 @@ let private helpLink link =
/// Render the page title, and optionally a help link /// Render the page title, and optionally a help link
let private renderPageTitle m pageTitle = let private renderPageTitle m pageTitle =
h2 [ _id "pt-page-title" ] [ h2 [ _id "pt-page-title" ] [
match m.HelpLink with Some link -> Help.fullLink (langCode ()) link |> helpLink | None -> () match m.HelpLink with Some link -> PrayerTracker.Utils.Help.fullLink (langCode ()) link |> helpLink | None -> ()
locStr pageTitle locStr pageTitle
] ]
@ -268,6 +253,9 @@ let private messages m =
] ]
]) ])
open System
/// 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 ()
@ -282,7 +270,7 @@ 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" ] [ _relNoOpener ] [
locStr s["Source & Support"] locStr s["Source & Support"]
] ]
] ]
@ -300,32 +288,56 @@ let private htmlFooter m =
script [ _src "/js/app.js" ] [] script [ _src "/js/app.js" ] []
] ]
/// The standard layout for PrayerTracker /// The content portion of the PrayerTracker layout
let standard m pageTitle (content : XmlNode) = let private contentSection viewInfo title (content : XmlNode) = [
let s = I18N.localizer.Force () Navigation.identity viewInfo
let ttl = s[pageTitle] renderPageTitle viewInfo title
yield! messages viewInfo
content
htmlFooter viewInfo
for jsFile in viewInfo.Script do
script [ _src $"/js/{jsFile}.js" ] []
]
/// The HTML head element for partial responses
let private partialHead pgTitle =
let s = I18N.localizer.Force ()
head [] [
meta [ _charset "UTF-8" ]
title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ]
]
open Giraffe.Htmx.Common
/// The body of the PrayerTracker layout
let private pageLayout viewInfo title content =
body [ _hxBoost ] [
Navigation.top viewInfo
div [ _id "pt-body"; Target.content; _hxSwap $"{HxSwap.InnerHtml} show:window:top" ]
(contentSection viewInfo title content)
]
/// The standard layout(s) for PrayerTracker
let standard viewInfo pageTitle content =
let s = I18N.localizer.Force ()
let pgTitle = s[pageTitle]
html [ _lang (langCode ()) ] [ html [ _lang (langCode ()) ] [
htmlHead m ttl match viewInfo.Layout with
body [ _hxBoost ] [ | FullPage ->
Navigation.top m htmlHead viewInfo pgTitle
div [ _id "pt-body" ] [ pageLayout viewInfo pgTitle content
Navigation.identity m | PartialPage ->
renderPageTitle m ttl partialHead pgTitle
yield! messages m pageLayout viewInfo pgTitle content
content | ContentOnly ->
htmlFooter m partialHead pgTitle
] body [] (contentSection viewInfo pgTitle content)
]
] ]
/// A layout with nothing but a title and content /// A layout with nothing but a title and content
let bare pageTitle content = let bare pageTitle content =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let ttl = s[pageTitle]
html [ _lang (langCode ()) ] [ html [ _lang (langCode ()) ] [
head [] [ partialHead s[pageTitle]
meta [ _charset "UTF-8" ]
title [] [ locStr ttl; titleSep; locStr s["PrayerTracker"] ]
]
body [] [ content ] body [] [ content ]
] ]

View File

@ -14,63 +14,66 @@ open PrayerTracker.ViewModels
let edit (m : EditRequest) today ctx vi = let edit (m : EditRequest) today ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request" let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request"
[ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ] input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ]
ReferenceList.requestTypeList s
|> Seq.ofList
|> Seq.map (fun (typ, desc) -> typ.code, desc.Value)
|> selectList (nameof m.RequestType) m.RequestType [ _required; _autofocus ]
]
div [ _class "pt-field" ] [
label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ]
input [ _type "text"
_name (nameof m.Requestor)
_id "requestor"
_value (defaultArg m.Requestor "") ]
]
if m.IsNew then
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "enteredDate" ] [ locStr s["Date"] ] label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ]
input [ _type "date"; _name (nameof m.EnteredDate); _id "enteredDate"; _placeholder today ] ReferenceList.requestTypeList s
|> Seq.ofList
|> Seq.map (fun (typ, desc) -> typ.code, desc.Value)
|> selectList (nameof m.RequestType) m.RequestType [ _required; _autofocus ]
] ]
else
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
div [ _class "pt-checkbox-field" ] [ label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ]
br [] input [ _type "text"
input [ _type "checkbox"; _name (nameof m.SkipDateUpdate); _id "skipDateUpdate"; _value "True" ] _id "requestor"
label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ] _name (nameof m.Requestor)
br [] _value (defaultArg m.Requestor "") ]
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ] ]
if m.IsNew then
div [ _class "pt-field" ] [
label [ _for "enteredDate" ] [ locStr s["Date"] ]
input [ _type "date"; _name (nameof m.EnteredDate); _id "enteredDate"; _placeholder today ]
] ]
else
div [ _class "pt-field" ] [
div [ _class "pt-checkbox-field" ] [
br []
input [ _type "checkbox"
_name (nameof m.SkipDateUpdate)
_id "skipDateUpdate"
_value "True" ]
label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ]
br []
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ]
]
]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [] [ locStr s["Expiration"] ]
ReferenceList.expirationList s (not m.IsNew)
|> List.map (fun exp ->
let radioId = $"expiration_{fst exp}"
span [ _class "text-nowrap" ] [
radio (nameof m.Expiration) radioId (fst exp) m.Expiration
label [ _for radioId ] [ locStr (snd exp) ]
rawText " &nbsp; &nbsp; "
])
|> div [ _class "pt-center-text" ]
] ]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [] [ locStr s["Expiration"] ]
ReferenceList.expirationList s (not m.IsNew)
|> List.map (fun exp ->
let radioId = $"expiration_{fst exp}"
span [ _class "text-nowrap" ] [
radio (nameof m.Expiration) radioId (fst exp) m.Expiration
label [ _for radioId ] [ locStr (snd exp) ]
rawText " &nbsp; &nbsp; "
])
|> div [ _class "pt-center-text" ]
] ]
] div [ _class "pt-field-row" ] [
div [ _class "pt-field-row" ] [ div [ _class "pt-field pt-editor" ] [
div [ _class "pt-field pt-editor" ] [ label [ _for "text" ] [ locStr s["Request"] ]
label [ _for "text" ] [ locStr s["Request"] ] textarea [ _name (nameof m.Text); _id "text" ] [ str m.Text ]
textarea [ _name (nameof m.Text); _id "text" ] [ str m.Text ] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Request"] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Request"] ] script [] [ rawText "PT.onLoad(PT.initCKEditor)" ]
]
script [] [ rawText "PT.onLoad(PT.initCKEditor)" ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle
@ -81,18 +84,18 @@ let email m vi =
let pageTitle = $"""{s["Prayer Requests"].Value} {m.SmallGroup.name}""" let pageTitle = $"""{s["Prayer Requests"].Value} {m.SmallGroup.name}"""
let prefs = m.SmallGroup.preferences let prefs = m.SmallGroup.preferences
let addresses = m.Recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>") |> String.concat ", " let addresses = m.Recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>") |> String.concat ", "
[ p [ _style $"font-family:{prefs.listFonts};font-size:%i{prefs.textFontSize}pt;" ] [ [ p [ _style $"font-family:{prefs.listFonts};font-size:%i{prefs.textFontSize}pt;" ] [
locStr s["The request list was sent to the following people, via individual e-mails"] locStr s["The request list was sent to the following people, via individual e-mails"]
rawText ":" rawText ":"
br [] br []
small [] [ str addresses ] small [] [ str addresses ]
] ]
span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ]
div [ _class "pt-email-canvas" ] [ rawText (m.AsHtml s) ] div [ _class "pt-email-canvas" ] [ rawText (m.AsHtml s) ]
br [] br []
br [] br []
span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ] span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ]
div [ _class "pt-email-canvas" ] [ pre [] [ str (m.AsText s) ] ] div [ _class "pt-email-canvas" ] [ pre [] [ str (m.AsText s) ] ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle
@ -100,8 +103,8 @@ let email m vi =
/// View for a small group's public prayer request list /// View for a small group's public prayer request list
let list (m : RequestList) vi = let list (m : RequestList) vi =
[ br [] [ br []
I18N.localizer.Force () |> (m.AsHtml >> rawText) I18N.localizer.Force () |> (m.AsHtml >> rawText)
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "View Request List" |> Layout.standard vi "View Request List"
@ -113,40 +116,42 @@ let lists (groups : SmallGroup list) vi =
let l = I18N.forView "Requests/Lists" let l = I18N.forView "Requests/Lists"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ p [] [ [ p [] [
raw l["The groups listed below have either public or password-protected request lists."] raw l["The groups listed below have either public or password-protected request lists."]
space space
raw l["Those with list icons are public, and those with log on icons are password-protected."] raw l["Those with list icons are public, and those with log on icons are password-protected."]
space space
raw l["Click the appropriate icon to log on or view the request list."] raw l["Click the appropriate icon to log on or view the request list."]
] ]
match groups.Length with match groups.Length with
| 0 -> p [] [ raw l["There are no groups with public or password-protected request lists."] ] | 0 -> p [] [ raw l["There are no groups with public or password-protected request lists."] ]
| count -> | count ->
tableSummary count s tableSummary count s
table [ _class "pt-table pt-action-table" ] [ table [ _class "pt-table pt-action-table" ] [
thead [] [ thead [] [
tr [] [ tr [] [
th [] [ locStr s["Actions"] ] th [] [ locStr s["Actions"] ]
th [] [ locStr s["Church"] ] th [] [ locStr s["Church"] ]
th [] [ locStr s["Group"] ] th [] [ locStr s["Group"] ]
] ]
] ]
groups groups
|> List.map (fun grp -> |> List.map (fun grp ->
let grpId = flatGuid grp.smallGroupId let grpId = flatGuid grp.smallGroupId
tr [] [ tr [] [
if grp.preferences.isPublic then if grp.preferences.isPublic then
a [ _href $"/prayer-requests/{grpId}/list"; _title s["View"].Value ] [ icon "list" ] a [ _href $"/prayer-requests/{grpId}/list"; _title s["View"].Value ] [ icon "list" ]
else else
a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [ icon "verified_user" ] a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [
|> List.singleton icon "verified_user"
|> td [] ]
td [] [ str grp.church.name ] |> List.singleton
td [] [ str grp.name ] |> td []
]) td [] [ str grp.church.name ]
|> tbody [] td [] [ str grp.name ]
] ])
|> tbody []
]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Request Lists" |> Layout.standard vi "Request Lists"
@ -159,14 +164,13 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
let now = m.SmallGroup.localDateNow (ctx.GetService<IClock> ()) let now = m.SmallGroup.localDateNow (ctx.GetService<IClock> ())
let prefs = m.SmallGroup.preferences
let types = ReferenceList.requestTypeList s |> Map.ofList let types = ReferenceList.requestTypeList s |> Map.ofList
let updReq (req : PrayerRequest) = let updReq (req : PrayerRequest) =
if req.updateRequired now m.SmallGroup.preferences.daysToExpire m.SmallGroup.preferences.longTermUpdateWeeks then if req.updateRequired now prefs.daysToExpire prefs.longTermUpdateWeeks then "pt-request-update" else ""
"pt-request-update"
else ""
|> _class |> _class
let reqExp (req : PrayerRequest) = let reqExp (req : PrayerRequest) =
_class (if req.isExpired now m.SmallGroup.preferences.daysToExpire then "pt-request-expired" else "") _class (if req.isExpired now prefs.daysToExpire then "pt-request-expired" else "")
/// Iterate the sequence once, before we render, so we can get the count of it at the top of the table /// Iterate the sequence once, before we render, so we can get the count of it at the top of the table
let requests = let requests =
m.Requests m.Requests
@ -175,28 +179,33 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
let reqText = htmlToPlainText req.text let reqText = htmlToPlainText req.text
let delAction = $"/prayer-request/{reqId}/delete" let delAction = $"/prayer-request/{reqId}/delete"
let delPrompt = let delPrompt =
[ s["Are you sure you want to delete this {0}? This action cannot be undone.", [ s["Are you sure you want to delete this {0}? This action cannot be undone.",
s["Prayer Request"].Value.ToLower() ].Value s["Prayer Request"].Value.ToLower() ].Value
"\\n" "\\n"
l["(If the prayer request has been answered, or an event has passed, consider inactivating it instead.)"] l["(If the prayer request has been answered, or an event has passed, consider inactivating it instead.)"]
.Value .Value
] ]
|> String.concat "" |> String.concat ""
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] [
[ icon "edit" ] icon "edit"
if req.isExpired now m.SmallGroup.preferences.daysToExpire then ]
a [ _href $"/prayer-request/{reqId}/restore" if req.isExpired now prefs.daysToExpire then
_title l["Restore This Inactive Request"].Value ] a [ _href $"/prayer-request/{reqId}/restore"
[ icon "visibility" ] _title l["Restore This Inactive Request"].Value ] [
icon "visibility"
]
else else
a [ _href $"/prayer-request/{reqId}/expire" a [ _href $"/prayer-request/{reqId}/expire"
_title l["Expire This Request Immediately"].Value ] _title l["Expire This Request Immediately"].Value ] [
[ icon "visibility_off" ] icon "visibility_off"
a [ _href delAction; _title l["Delete This Request"].Value; ]
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] a [ _href delAction
[ icon "delete_forever" ] _title l["Delete This Request"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
] ]
td [ updReq req ] [ td [ updReq req ] [
str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture)) str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture))
@ -210,79 +219,83 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
] ]
]) ])
|> List.ofSeq |> List.ofSeq
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Request"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Request"]
rawText " &nbsp; &nbsp; &nbsp; " ]
a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ]
[ icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"] ]
match m.SearchTerm with
| Some _ ->
rawText " &nbsp; &nbsp; &nbsp; " rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [
[ icon "highlight_off"; rawText " &nbsp;"; raw l["Clear Search Criteria"] ] icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"]
| None -> () ]
] match m.SearchTerm with
form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form" ] [ | Some _ ->
input [ _type "text" rawText " &nbsp; &nbsp; &nbsp; "
_name "search" a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] [
_placeholder l["Search requests..."].Value icon "highlight_off"; rawText " &nbsp;"; raw l["Clear Search Criteria"]
_value (defaultArg m.SearchTerm "")
] ]
space | None -> ()
submit [] "search" s["Search"] ]
] form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form"; Target.content ] [
br [] input [ _type "text"
tableSummary requests.Length s _name "search"
match requests.Length with _placeholder l["Search requests..."].Value
| 0 -> () _value (defaultArg m.SearchTerm "")
| _ ->
table [ _class "pt-table pt-action-table" ] [
thead [] [
tr [] [
th [] [ locStr s["Actions"] ]
th [] [ locStr s["Updated Date"] ]
th [] [ locStr s["Type"] ]
th [] [ locStr s["Requestor"] ]
th [] [ locStr s["Request"] ]
] ]
] space
tbody [] requests submit [] "search" s["Search"]
] ]
div [ _class "pt-center-text" ] [ br []
br [] tableSummary requests.Length s
match m.OnlyActive with match requests.Length with
| Some true -> | 0 -> ()
raw l["Inactive requests are currently not shown"] | _ ->
br [] table [ _class "pt-table pt-action-table" ] [
a [ _href "/prayer-requests/inactive" ] [ raw l["Show Inactive Requests"] ] thead [] [
| _ -> tr [] [
if defaultArg m.OnlyActive false then th [] [ locStr s["Actions"] ]
raw l["Inactive requests are currently shown"] th [] [ locStr s["Updated Date"] ]
br [] th [] [ locStr s["Type"] ]
a [ _href "/prayer-requests" ] [ raw l["Do Not Show Inactive Requests"] ] th [] [ locStr s["Requestor"] ]
br [] th [] [ locStr s["Request"] ]
br [] ]
let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ] ]
let pg = defaultArg m.PageNbr 1 tbody [] requests
let url = ]
match m.OnlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/prayer-requests%s" div [ _class "pt-center-text" ] [
match pg with br []
| 1 -> () match m.OnlyActive with
| _ -> | Some true ->
// button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ] raw l["Inactive requests are currently not shown"]
let withPage = match pg with 2 -> search | _ -> ("page", string (pg - 1)) :: search br []
a [ _href (makeUrl url withPage) ] a [ _href "/prayer-requests/inactive" ] [ raw l["Show Inactive Requests"] ]
[ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ] | _ ->
rawText " &nbsp; &nbsp; " if defaultArg m.OnlyActive false then
match requests.Length = m.SmallGroup.preferences.pageSize with raw l["Inactive requests are currently shown"]
| true -> br []
a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] a [ _href "/prayer-requests" ] [ raw l["Do Not Show Inactive Requests"] ]
[ raw l["Next Page"]; space; icon "keyboard_arrow_right" ] br []
| false -> () br []
] let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ]
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] let pg = defaultArg m.PageNbr 1
let url =
match m.OnlyActive with Some true | None -> "" | _ -> "/inactive"
|> sprintf "/prayer-requests%s"
match pg with
| 1 -> ()
| _ ->
// button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ]
let withPage = match pg with 2 -> search | _ -> ("page", string (pg - 1)) :: search
a [ _href (makeUrl url withPage) ] [ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ]
rawText " &nbsp; &nbsp; "
match requests.Length = m.SmallGroup.preferences.pageSize with
| true ->
a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] [
raw l["Next Page"]; space; icon "keyboard_arrow_right"
]
| false -> ()
]
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
|> Layout.Content.wide |> Layout.Content.wide
|> Layout.standard vi (match m.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests") |> Layout.standard vi (match m.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests")
@ -315,38 +328,40 @@ let view m vi =
let pageTitle = $"""{s["Prayer Requests"].Value} {m.SmallGroup.name}""" let pageTitle = $"""{s["Prayer Requests"].Value} {m.SmallGroup.name}"""
let spacer = rawText " &nbsp; &nbsp; &nbsp; " let spacer = rawText " &nbsp; &nbsp; &nbsp; "
let dtString = m.Date.ToString "yyyy-MM-dd" let dtString = m.Date.ToString "yyyy-MM-dd"
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _class "pt-icon-link" a [ _class "pt-icon-link"
_href $"/prayer-requests/print/{dtString}" _href $"/prayer-requests/print/{dtString}"
_title s["View Printable"].Value _target "_blank"
] [ icon "print"; rawText " &nbsp;"; locStr s["View Printable"] ] _title s["View Printable"].Value ] [
if m.CanEmail then icon "print"; rawText " &nbsp;"; locStr s["View Printable"]
spacer ]
if m.Date.DayOfWeek <> DayOfWeek.Sunday then if m.CanEmail then
let rec findSunday (date : DateTime) = spacer
if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.) if m.Date.DayOfWeek <> DayOfWeek.Sunday then
let sunday = findSunday m.Date let rec findSunday (date : DateTime) =
a [ _class "pt-icon-link" if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.)
_href $"""/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}""" let sunday = findSunday m.Date
_title s["List for Next Sunday"].Value ] [ a [ _class "pt-icon-link"
icon "update"; rawText " &nbsp;"; locStr s["List for Next Sunday"] _href $"""/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}"""
] _title s["List for Next Sunday"].Value ] [
spacer icon "update"; rawText " &nbsp;"; locStr s["List for Next Sunday"]
let emailPrompt = s["This will e-mail the current list to every member of your group, without further prompting. Are you sure this is what you are ready to do?"].Value ]
a [ _class "pt-icon-link" spacer
_href $"/prayer-requests/email/{dtString}" let emailPrompt = s["This will e-mail the current list to every member of your group, without further prompting. Are you sure this is what you are ready to do?"].Value
_title s["Send via E-mail"].Value a [ _class "pt-icon-link"
_onclick $"return PT.requests.view.promptBeforeEmail('{emailPrompt}')" ] [ _href $"/prayer-requests/email/{dtString}"
icon "mail_outline"; rawText " &nbsp;"; locStr s["Send via E-mail"] _title s["Send via E-mail"].Value
_onclick $"return PT.requests.view.promptBeforeEmail('{emailPrompt}')" ] [
icon "mail_outline"; rawText " &nbsp;"; locStr s["Send via E-mail"]
]
spacer
a [ _class "pt-icon-link"; _href "/prayer-requests"; _title s["Maintain Prayer Requests"].Value ] [
icon "compare_arrows"; rawText " &nbsp;"; locStr s["Maintain Prayer Requests"]
] ]
spacer
a [ _class "pt-icon-link"; _href "/prayer-requests"; _title s["Maintain Prayer Requests"].Value ] [
icon "compare_arrows"; rawText " &nbsp;"; locStr s["Maintain Prayer Requests"]
] ]
] br []
br [] rawText (m.AsHtml s)
rawText (m.AsHtml s)
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle

View File

@ -147,7 +147,7 @@
<data name="If you utilize the “{0}” box on sign in, a second cookie is stored, and transmitted to establish a session; this cookie is removed by clicking the “{1}” link." xml:space="preserve"> <data name="If you utilize the “{0}” box on sign in, a second cookie is stored, and transmitted to establish a session; this cookie is removed by clicking the “{1}” link." xml:space="preserve">
<value>Si utilizas el cuadro "{0}" al iniciar sesión, se almacena una segunda cookie y se transmite para establecer una sesión; esta cookie se elimina haciendo clic en el enlace "{1}".</value> <value>Si utilizas el cuadro "{0}" al iniciar sesión, se almacena una segunda cookie y se transmite para establecer una sesión; esta cookie se elimina haciendo clic en el enlace "{1}".</value>
</data> </data>
<data name="It also stores names and e-mail addreses of small group members, and plain-text passwords for small groups with password-protected lists." xml:space="preserve"> <data name="It also stores names and e-mail addresses of small group members, and plain-text passwords for small groups with password-protected lists." xml:space="preserve">
<value>También almacena nombres y direcciones de correo electrónico de miembros de grupos pequeños, y contraseñas de texto sin formato para grupos pequeños con listas protegidas por contraseña.</value> <value>También almacena nombres y direcciones de correo electrónico de miembros de grupos pequeños, y contraseñas de texto sin formato para grupos pequeños con listas protegidas por contraseña.</value>
</data> </data>
<data name="On the server, all data is stored in a controlled-access database." xml:space="preserve"> <data name="On the server, all data is stored in a controlled-access database." xml:space="preserve">

View File

@ -1,9 +1,7 @@
module PrayerTracker.Views.SmallGroup module PrayerTracker.Views.SmallGroup
open System.IO
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Microsoft.Extensions.Localization open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
@ -12,45 +10,45 @@ let announcement isAdmin ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None } let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None }
let reqTypes = ReferenceList.requestTypeList s let reqTypes = ReferenceList.requestTypeList s
[ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [
div [ _class "pt-field pt-editor" ] [
label [ _for "text" ] [ locStr s["Announcement Text"] ]
textarea [ _name (nameof m.Text); _id "text"; _autofocus ] []
]
]
if isAdmin then
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field pt-editor" ] [
label [] [ locStr s["Send Announcement to"]; rawText ":" ] label [ _for "text" ] [ locStr s["Announcement Text"] ]
div [ _class "pt-center-text" ] [ textarea [ _name (nameof m.Text); _id "text"; _autofocus ] []
radio (nameof m.SendToClass) "sendY" "Y" "Y"
label [ _for "sendY" ] [ locStr s["This Group"]; rawText " &nbsp; &nbsp; " ]
radio (nameof m.SendToClass) "sendN" "N" "Y"
label [ _for "sendN" ] [ locStr s["All {0} Users", s["PrayerTracker"]] ]
]
] ]
] ]
else input [ _type "hidden"; _name (nameof m.SendToClass); _value "Y" ] if isAdmin then
div [ _class "pt-field-row pt-fadeable pt-shown"; _id "divAddToList" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-checkbox-field" ] [ div [ _class "pt-field" ] [
input [ _type "checkbox"; _name (nameof m.AddToRequestList); _id "addToRequestList"; _value "True" ] label [] [ locStr s["Send Announcement to"]; rawText ":" ]
label [ _for "addToRequestList" ] [ locStr s["Add to Request List"] ] div [ _class "pt-center-text" ] [
radio (nameof m.SendToClass) "sendY" "Y" "Y"
label [ _for "sendY" ] [ locStr s["This Group"]; rawText " &nbsp; &nbsp; " ]
radio (nameof m.SendToClass) "sendN" "N" "Y"
label [ _for "sendN" ] [ locStr s["All {0} Users", s["PrayerTracker"]] ]
]
]
]
else input [ _type "hidden"; _name (nameof m.SendToClass); _value "Y" ]
div [ _class "pt-field-row pt-fadeable pt-shown"; _id "divAddToList" ] [
div [ _class "pt-checkbox-field" ] [
input [ _type "checkbox"; _name (nameof m.AddToRequestList); _id "addToRequestList"; _value "True" ]
label [ _for "addToRequestList" ] [ locStr s["Add to Request List"] ]
]
] ]
] div [ _class "pt-field-row pt-fadeable"; _id "divCategory" ] [
div [ _class "pt-field-row pt-fadeable"; _id "divCategory" ] [ div [ _class "pt-field" ] [
div [ _class "pt-field" ] [ label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ]
label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ] reqTypes
reqTypes |> Seq.ofList
|> Seq.ofList |> Seq.map (fun (typ, desc) -> typ.code, desc.Value)
|> Seq.map (fun (typ, desc) -> typ.code, desc.Value) |> selectList (nameof m.RequestType) Announcement.code []
|> selectList (nameof m.RequestType) Announcement.code [] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "send" s["Send Announcement"] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "send" s["Send Announcement"] ] script [] [ rawText "PT.onLoad(PT.smallGroup.announcement.onPageLoad)" ]
]
script [] [ rawText "PT.onLoad(PT.smallGroup.announcement.onPageLoad)" ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Send Announcement" |> Layout.standard vi "Send Announcement"
@ -59,12 +57,12 @@ let announcement isAdmin ctx vi =
/// View for once an announcement has been sent /// View for once an announcement has been sent
let announcementSent (m : Announcement) vi = let announcementSent (m : Announcement) vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
[ span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] [ span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ]
div [ _class "pt-email-canvas" ] [ rawText m.Text ] div [ _class "pt-email-canvas" ] [ rawText m.Text ]
br [] br []
br [] br []
span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ] span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ]
div [ _class "pt-email-canvas" ] [ pre [] [ str m.PlainText ] ] div [ _class "pt-email-canvas" ] [ pre [] [ str m.PlainText ] ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Announcement Sent" |> Layout.standard vi "Announcement Sent"
@ -74,7 +72,7 @@ let announcementSent (m : Announcement) vi =
let edit (m : EditSmallGroup) (churches : Church list) ctx vi = let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group" let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group"
form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ] input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -104,7 +102,7 @@ let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi = let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member" let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member"
form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ] style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ]
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ] input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ]
@ -137,40 +135,39 @@ let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi
let logOn (groups : SmallGroup list) grpId ctx vi = let logOn (groups : SmallGroup list) grpId ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None } let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None }
[ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns"; Target.body ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq { seq {
match groups.Length with if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value
| 0 -> "", s["There are no classes with passwords defined"].Value else
| _ -> "", selectDefault s["Select Group"].Value
"", selectDefault s["Select Group"].Value yield!
yield! groups
groups |> List.map (fun grp -> flatGuid grp.smallGroupId, $"{grp.church.name} | {grp.name}")
|> List.map (fun grp -> flatGuid grp.smallGroupId, $"{grp.church.name} | {grp.name}") }
} |> selectList (nameof m.SmallGroupId) grpId [ _required ]
|> selectList (nameof m.SmallGroupId) grpId [ _required ] ]
div [ _class "pt-field" ] [
label [ _for "password" ] [ locStr s["Password"] ]
input [ _type "password"
_name (nameof m.Password)
_id "password"
_placeholder (s["Case-Sensitive"].Value.ToLower ())
_required ]
]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-checkbox-field" ] [
label [ _for "password" ] [ locStr s["Password"] ] input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ]
input [ _type "password" label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
_name (nameof m.Password) br []
_id "password" small [] [ em [] [ str (s["Requires Cookies"].Value.ToLower ()) ] ]
_required;
_placeholder (s["Case-Sensitive"].Value.ToLower ()) ]
] ]
div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ]
] ]
div [ _class "pt-checkbox-field" ] [ script [] [ rawText "PT.onLoad(PT.smallGroup.logOn.onPageLoad)" ]
input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ]
label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
br []
small [] [ em [] [ str (s["Requires Cookies"].Value.ToLower ()) ] ]
]
div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ]
]
script [] [ rawText "PT.onLoad(PT.smallGroup.logOn.onPageLoad)" ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Group Log On" |> Layout.standard vi "Group Log On"
@ -200,12 +197,12 @@ let maintain (groups : SmallGroup list) ctx vi =
$"""{s["Small Group"].Value.ToLower ()} ({g.name})""" ].Value $"""{s["Small Group"].Value.ToLower ()} ({g.name})""" ].Value
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] [ icon "edit" ]
[ icon "edit" ] a [ _href delAction
a [ _href delAction _title s["Delete This Group"].Value
_title s["Delete This Group"].Value _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] icon "delete_forever"
[ icon "delete_forever" ] ]
] ]
td [] [ str g.name ] td [] [ str g.name ]
td [] [ str g.church.name ] td [] [ str g.church.name ]
@ -213,19 +210,17 @@ let maintain (groups : SmallGroup list) ctx vi =
]) ])
|> tbody [] |> tbody []
] ]
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/small-group/{emptyGuid}/edit"; _title s["Add a New Group"].Value ] [ a [ _href $"/small-group/{emptyGuid}/edit"; _title s["Add a New Group"].Value ] [
icon "add_circle" icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group"]
rawText " &nbsp;" ]
locStr s["Add a New Group"] br []
] br []
br [] ]
br [] tableSummary groups.Length s
] grpTbl
tableSummary groups.Length s form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
grpTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Maintain Groups" |> Layout.standard vi "Maintain Groups"
@ -256,12 +251,14 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
.Value.Replace("?", $" ({mbr.memberName})?") .Value.Replace("?", $" ({mbr.memberName})?")
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ] a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ] [
[ icon "edit" ] icon "edit"
a [ _href delAction ]
_title s["Delete This Group Member"].Value a [ _href delAction
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _title s["Delete This Group Member"].Value
[ icon "delete_forever" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
] ]
td [] [ str mbr.memberName ] td [] [ str mbr.memberName ]
td [] [ str mbr.email ] td [] [ str mbr.email ]
@ -269,73 +266,64 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
]) ])
|> tbody [] |> tbody []
] ]
[ div [ _class"pt-center-text" ] [ [ div [ _class"pt-center-text" ] [
br [] br []
a [ _href $"/small-group/member/{emptyGuid}/edit"; _title s["Add a New Group Member"].Value ] a [ _href $"/small-group/member/{emptyGuid}/edit"; _title s["Add a New Group Member"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group Member"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group Member"]
br [] ]
br [] br []
] br []
tableSummary members.Length s ]
mbrTbl tableSummary members.Length s
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] mbrTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Maintain Group Members" |> Layout.standard vi "Maintain Group Members"
open Giraffe.ViewEngine.Accessibility
/// View for the small group overview page /// View for the small group overview page
let overview m vi = let overview m vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let linkSpacer = rawText "&nbsp; " let linkSpacer = rawText "&nbsp; "
let types = ReferenceList.requestTypeList s |> dict let types = ReferenceList.requestTypeList s |> dict
article [ _class "pt-overview" ] [ article [ _class "pt-overview" ] [
section [] [ section [ _ariaLabel "Quick actions" ] [
header [ _role "heading" ] [ header [ _roleHeading ] [ iconSized 72 "bookmark_border"; locStr s["Quick Actions"] ]
iconSized 72 "bookmark_border"
locStr s["Quick Actions"]
]
div [] [ div [] [
a [ _href "/prayer-requests/view" ] a [ _href "/prayer-requests/view" ] [ icon "list"; linkSpacer; locStr s["View Prayer Request List"] ]
[ icon "list"; linkSpacer; locStr s["View Prayer Request List"] ]
hr [] hr []
a [ _href "/small-group/announcement" ] [ icon "send"; linkSpacer; locStr s["Send Announcement"] ] a [ _href "/small-group/announcement" ] [ icon "send"; linkSpacer; locStr s["Send Announcement"] ]
hr [] hr []
a [ _href "/small-group/preferences" ] [ icon "build"; linkSpacer; locStr s["Change Preferences"] ] a [ _href "/small-group/preferences" ] [ icon "build"; linkSpacer; locStr s["Change Preferences"] ]
] ]
] ]
section [] [ section [ _ariaLabel "Prayer requests" ] [
header [ _role "heading" ] [ header [ _roleHeading ] [ iconSized 72 "question_answer"; locStr s["Prayer Requests"] ]
iconSized 72 "question_answer"
locStr s["Prayer Requests"]
]
div [] [ div [] [
p [ _class "pt-center-text" ] [ p [ _class "pt-center-text" ] [
strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ] strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ]
] ]
hr [] hr []
for cat in m.ActiveReqsByType do for cat in m.ActiveReqsByType do
str (cat.Value.ToString "N0") str (cat.Value.ToString "N0")
space space
locStr types[cat.Key] locStr types[cat.Key]
br [] br []
br [] br []
str (m.AllReqs.ToString "N0") str (m.AllReqs.ToString "N0")
space space
locStr s["Total Requests"] locStr s["Total Requests"]
hr [] hr []
a [ _href "/prayer-requests/maintain" ] [ a [ _href "/prayer-requests/maintain" ] [
icon "compare_arrows" icon "compare_arrows"; linkSpacer; locStr s["Maintain Prayer Requests"]
linkSpacer
locStr s["Maintain Prayer Requests"]
] ]
] ]
] ]
section [] [ section [ _ariaLabel "Small group members" ] [
header [ _role "heading" ] [ header [ _roleHeading ] [ iconSized 72 "people_outline"; locStr s["Group Members"] ]
iconSized 72 "people_outline"
locStr s["Group Members"]
]
div [ _class "pt-center-text" ] [ div [ _class "pt-center-text" ] [
strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ] strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ]
hr [] hr []
@ -348,240 +336,246 @@ let overview m vi =
|> Layout.standard vi "Small Group Overview" |> Layout.standard vi "Small Group Overview"
open System.IO
open PrayerTracker
/// View for the small group preferences page /// View for the small group preferences page
let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let l = I18N.forView "SmallGroup/Preferences" let l = I18N.forView "SmallGroup/Preferences"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns" ] [ [ style [ _scoped ] [
style [ _scoped ] rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }"
[ rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }" ] ]
csrfToken ctx form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
fieldset [] [ csrfToken ctx
legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ] fieldset [] [
div [ _class "pt-field-row" ] [ legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ]
div [ _class "pt-field" ] [ div [ _class "pt-field-row" ] [
label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ] div [ _class "pt-field" ] [
span [] [ label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ]
input [ _type "number" span [] [
_name (nameof m.ExpireDays) input [ _type "number"
_id "expireDays" _name (nameof m.ExpireDays)
_min "1"; _max "30" _id "expireDays"
_required _value (string m.ExpireDays)
_autofocus _min "1"; _max "30"
_value (string m.ExpireDays) ] _required
space; str (s["Days"].Value.ToLower ()) _autofocus ]
space
str (s["Days"].Value.ToLower ())
]
] ]
] div [ _class "pt-field" ] [
div [ _class "pt-field" ] [ label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ]
label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ] span [] [
span [] [ input [ _type "number"
input [ _type "number" _name (nameof m.DaysToKeepNew)
_name (nameof m.DaysToKeepNew) _id "daysToKeepNew"
_id "daysToKeepNew" _min "1"; _max "30"
_min "1"; _max "30" _value (string m.DaysToKeepNew)
_required _required ]
_value (string m.DaysToKeepNew) ] space; str (s["Days"].Value.ToLower ())
space; str (s["Days"].Value.ToLower ()) ]
] ]
] div [ _class "pt-field" ] [
div [ _class "pt-field" ] [ label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ]
label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ] span [] [
span [] [ input [ _type "number"
input [ _type "number" _name (nameof m.LongTermUpdateWeeks)
_name (nameof m.LongTermUpdateWeeks) _id "longTermUpdateWeeks"
_id "longTermUpdateWeeks" _min "1"; _max "30"
_min "1"; _max "30" _value (string m.LongTermUpdateWeeks)
_required _required ]
_value (string m.LongTermUpdateWeeks) ] space; str (s["Weeks"].Value.ToLower ())
space; str (s["Weeks"].Value.ToLower ()) ]
] ]
] ]
] ]
] fieldset [] [
fieldset [] [ legend [] [ strong [] [ icon "sort"; rawText " &nbsp;"; locStr s["Request Sorting"] ] ]
legend [] [ strong [] [ icon "sort"; rawText " &nbsp;"; locStr s["Request Sorting"] ] ] radio (nameof m.RequestSort) "requestSort_D" "D" m.RequestSort
radio (nameof m.RequestSort) "requestSort_D" "D" m.RequestSort label [ _for "requestSort_D" ] [ locStr s["Sort by Last Updated Date"] ]
label [ _for "requestSort_D" ] [ locStr s["Sort by Last Updated Date"] ] rawText " &nbsp; "
rawText " &nbsp; " radio (nameof m.RequestSort) "requestSort_R" "R" m.RequestSort
radio (nameof m.RequestSort) "requestSort_R" "R" m.RequestSort label [ _for "requestSort_R" ] [ locStr s["Sort by Requestor Name"] ]
label [ _for "requestSort_R" ] [ locStr s["Sort by Requestor Name"] ]
]
fieldset [] [
legend [] [ strong [] [ icon "mail_outline"; rawText " &nbsp;"; locStr s["E-mail"] ] ]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for "emailFromName" ] [ locStr s["From Name"] ]
input [ _type "text"
_name (nameof m.EmailFromName)
_id "emailFromName"
_required
_value m.EmailFromName ]
]
div [ _class "pt-field" ] [
label [ _for "emailFromAddress" ] [ locStr s["From Address"] ]
input [ _type "email"
_name (nameof m.EmailFromAddress)
_id "emailFromAddress"
_required
_value m.EmailFromAddress ]
]
] ]
div [ _class "pt-field-row" ] [ fieldset [] [
div [ _class "pt-field" ] [ legend [] [ strong [] [ icon "mail_outline"; rawText " &nbsp;"; locStr s["E-mail"] ] ]
label [ _for (nameof m.DefaultEmailType) ] [ locStr s["E-mail Format"] ] div [ _class "pt-field-row" ] [
seq { div [ _class "pt-field" ] [
"", selectDefault s["Select"].Value label [ _for "emailFromName" ] [ locStr s["From Name"] ]
yield! input [ _type "text"
ReferenceList.emailTypeList HtmlFormat s _name (nameof m.EmailFromName)
|> Seq.skip 1 _id "emailFromName"
|> Seq.map (fun typ -> fst typ, (snd typ).Value) _value m.EmailFromName
} _required ]
|> selectList (nameof m.DefaultEmailType) m.DefaultEmailType [ _required ] ]
div [ _class "pt-field" ] [
label [ _for "emailFromAddress" ] [ locStr s["From Address"] ]
input [ _type "email"
_name (nameof m.EmailFromAddress)
_id "emailFromAddress"
_value m.EmailFromAddress
_required ]
]
] ]
] div [ _class "pt-field-row" ] [
] div [ _class "pt-field" ] [
fieldset [] [ label [ _for (nameof m.DefaultEmailType) ] [ locStr s["E-mail Format"] ]
legend [] [ strong [] [ icon "color_lens"; rawText " &nbsp;"; locStr s["Colors"] ]; rawText " ***" ] seq {
div [ _class "pt-field-row" ] [ "", selectDefault s["Select"].Value
div [ _class "pt-field" ] [ yield!
label [ _class "pt-center-text" ] [ locStr s["Color of Heading Lines"] ] ReferenceList.emailTypeList HtmlFormat s
span [] [ |> Seq.skip 1
radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType |> Seq.map (fun typ -> fst typ, (snd typ).Value)
label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ] }
namedColorList (nameof m.LineColor) m.LineColor |> selectList (nameof m.DefaultEmailType) m.DefaultEmailType [ _required ]
[ _id "lineColor_Select"
if m.LineColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType
label [ _for "lineColorType_RGB" ] [ locStr s["Custom Color"] ]
input [ _type "color"
_name (nameof m.LineColor)
_id "lineColor_Color"
_value m.LineColor // TODO: convert to hex or skip if named
if not (m.LineColor.StartsWith "#") then _disabled ]
] ]
] ]
] ]
div [ _class "pt-field-row" ] [ fieldset [] [
legend [] [ strong [] [ icon "color_lens"; rawText " &nbsp;"; locStr s["Colors"] ]; rawText " ***" ]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _class "pt-center-text" ] [ locStr s["Color of Heading Lines"] ]
span [] [
radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType
label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList (nameof m.LineColor) m.LineColor [
_id "lineColor_Select"
if m.LineColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType
label [ _for "lineColorType_RGB" ] [ locStr s["Custom Color"] ]
input [ _type "color"
_name (nameof m.LineColor)
_id "lineColor_Color"
_value m.LineColor // TODO: convert to hex or skip if named
if not (m.LineColor.StartsWith "#") then _disabled ]
]
]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _class "pt-center-text" ] [ locStr s["Color of Heading Text"] ]
span [] [
radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType
label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList (nameof m.HeadingColor) m.HeadingColor [
_id "headingColor_Select"
if m.HeadingColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType
label [ _for "headingColorType_RGB" ] [ locStr s["Custom Color"] ]
input [ _type "color"
_name (nameof m.HeadingColor)
_id "headingColor_Color"
_value m.HeadingColor // TODO: convert to hex or skip if named
if not (m.HeadingColor.StartsWith "#") then _disabled ]
]
]
]
]
fieldset [] [
legend [] [ strong [] [ icon "font_download"; rawText " &nbsp;"; locStr s["Fonts"] ] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _class "pt-center-text" ] [ locStr s["Color of Heading Text"] ] label [ _for "fonts" ] [ locStr s["Fonts** for List"] ]
input [ _type "text"; _name (nameof m.Fonts); _id "fonts"; _required; _value m.Fonts ]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for "headingFontSize" ] [ locStr s["Heading Text Size"] ]
input [ _type "number"
_name (nameof m.HeadingFontSize)
_id "headingFontSize"
_min "8"; _max "24"
_value (string m.HeadingFontSize)
_required ]
]
div [ _class "pt-field" ] [
label [ _for "listFontSize" ] [ locStr s["List Text Size"] ]
input [ _type "number"
_name (nameof m.ListFontSize)
_id "listFontSize"
_min "8"; _max "24"
_value (string m.ListFontSize)
_required ]
]
]
]
fieldset [] [
legend [] [ strong [] [ icon "settings"; rawText " &nbsp;"; locStr s["Other Settings"] ] ]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for (nameof m.TimeZone) ] [ locStr s["Time Zone"] ]
seq {
"", selectDefault s["Select"].Value
yield! tzs |> List.map (fun tz -> tz.timeZoneId, (TimeZones.name tz.timeZoneId s).Value)
}
|> selectList (nameof m.TimeZone) m.TimeZone [ _required ]
]
]
div [ _class "pt-field" ] [
label [] [ locStr s["Request List Visibility"] ]
span [] [ span [] [
radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``)
label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ] (string m.Visibility)
namedColorList (nameof m.HeadingColor) m.HeadingColor label [ _for "viz_Public" ] [ locStr s["Public"] ]
[ _id "headingColor_Select" rawText " &nbsp;"
if m.HeadingColor.StartsWith "#" then _disabled ] s radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``)
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ()) (string m.Visibility)
radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType label [ _for "viz_Private" ] [ locStr s["Private"] ]
label [ _for "headingColorType_RGB" ] [ locStr s["Custom Color"] ] rawText " &nbsp;"
input [ _type "color" radio (nameof m.Visibility) "viz_Password" (string RequestVisibility.passwordProtected)
_name (nameof m.HeadingColor) (string m.Visibility)
_id "headingColor_Color" label [ _for "viz_Password" ] [ locStr s["Password Protected"] ]
_value m.HeadingColor // TODO: convert to hex or skip if named ]
if not (m.HeadingColor.StartsWith "#") then _disabled ] ]
] let classSuffix = if m.Visibility = RequestVisibility.passwordProtected then " pt-show" else ""
div [ _id "divClassPassword"; _class $"pt-field-row pt-fadeable{classSuffix}" ] [
div [ _class "pt-field" ] [
label [ _for "groupPassword" ] [ locStr s["Group Password (Used to Read Online)"] ]
input [ _type "text"
_name (nameof m.GroupPassword)
_id "groupPassword"
_value (defaultArg m.GroupPassword "") ]
]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for "pageSize" ] [ locStr s["Page Size"] ]
input [ _type "number"
_name (nameof m.PageSize)
_id "pageSize"
_min "10"; _max "255"
_value (string m.PageSize)
_required ]
]
div [ _class "pt-field" ] [
label [ _for (nameof m.AsOfDate) ] [ locStr s["“As of” Date Display"] ]
ReferenceList.asOfDateList s
|> List.map (fun (code, desc) -> code, desc.Value)
|> selectList (nameof m.AsOfDate) m.AsOfDate [ _required ]
]
] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Preferences"] ]
] ]
fieldset [] [ p [] [
legend [] [ strong [] [ icon "font_download"; rawText " &nbsp;"; locStr s["Fonts"] ] ] rawText "** "
div [ _class "pt-field" ] [ raw l["List font names, separated by commas."]
label [ _for "fonts" ] [ locStr s["Fonts** for List"] ] space
input [ _type "text"; _name (nameof m.Fonts); _id "fonts"; _required; _value m.Fonts ] raw l["The first font that is matched is the one that is used."]
] space
div [ _class "pt-field-row" ] [ raw l["Ending with either “serif” or “sans-serif” will cause the user's browser to use the default “serif” font (“Times New Roman” on Windows) or “sans-serif” font (“Arial” on Windows) if no other fonts in the list are found."]
div [ _class "pt-field" ] [
label [ _for "headingFontSize" ] [ locStr s["Heading Text Size"] ]
input [ _type "number"
_name (nameof m.HeadingFontSize)
_id "headingFontSize"
_min "8"; _max "24"
_required
_value (string m.HeadingFontSize) ]
]
div [ _class "pt-field" ] [
label [ _for "listFontSize" ] [ locStr s["List Text Size"] ]
input [ _type "number"
_name (nameof m.ListFontSize)
_id "listFontSize"
_min "8"; _max "24"
_required
_value (string m.ListFontSize) ]
]
]
] ]
fieldset [] [ p [] [
legend [] [ strong [] [ icon "settings"; rawText " &nbsp;"; locStr s["Other Settings"] ] ] rawText "*** "
div [ _class "pt-field-row" ] [ raw l["If you want a custom color, you may be able to get some ideas (and a list of RGB values for those colors) from the W3 School's <a href=\"http://www.w3schools.com/html/html_colornames.asp\" title=\"HTML Color List - W3 School\">HTML color name list</a>."]
div [ _class "pt-field" ] [
label [ _for (nameof m.TimeZone) ] [ locStr s["Time Zone"] ]
seq {
"", selectDefault s["Select"].Value
yield! tzs |> List.map (fun tz -> tz.timeZoneId, (TimeZones.name tz.timeZoneId s).Value)
}
|> selectList (nameof m.TimeZone) m.TimeZone [ _required ]
]
]
div [ _class "pt-field" ] [
label [] [ locStr s["Request List Visibility"] ]
span [] [
radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``) (string m.Visibility)
label [ _for "viz_Public" ] [ locStr s["Public"] ]
rawText " &nbsp;"
radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``)
(string m.Visibility)
label [ _for "viz_Private" ] [ locStr s["Private"] ]
rawText " &nbsp;"
radio (nameof m.Visibility) "viz_Password" (string RequestVisibility.passwordProtected)
(string m.Visibility)
label [ _for "viz_Password" ] [ locStr s["Password Protected"] ]
]
]
let classSuffix = if m.Visibility = RequestVisibility.passwordProtected then " pt-show" else ""
div [ _id "divClassPassword"; _class $"pt-field-row pt-fadeable{classSuffix}" ] [
div [ _class "pt-field" ] [
label [ _for "groupPassword" ] [ locStr s["Group Password (Used to Read Online)"] ]
input [ _type "text"
_name (nameof m.GroupPassword)
_id "groupPassword"
_value (defaultArg m.GroupPassword "") ]
]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for "pageSize" ] [ locStr s["Page Size"] ]
input [ _type "number"
_name (nameof m.PageSize)
_id "pageSize"
_min "10"; _max "255"
_required
_value (string m.PageSize) ]
]
div [ _class "pt-field" ] [
label [ _for (nameof m.AsOfDate) ] [ locStr s["“As of” Date Display"] ]
ReferenceList.asOfDateList s
|> List.map (fun (code, desc) -> code, desc.Value)
|> selectList (nameof m.AsOfDate) m.AsOfDate [ _required ]
]
]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Preferences"] ] script [] [ rawText "PT.onLoad(PT.smallGroup.preferences.onPageLoad)" ]
]
p [] [
rawText "** "
raw l["List font names, separated by commas."]
space
raw l["The first font that is matched is the one that is used."]
space
raw l["Ending with either “serif” or “sans-serif” will cause the user's browser to use the default “serif” font (“Times New Roman” on Windows) or “sans-serif” font (“Arial” on Windows) if no other fonts in the list are found."]
]
p [] [
rawText "*** "
raw l["If you want a custom color, you may be able to get some ideas (and a list of RGB values for those colors) from the W3 School's <a href=\"http://www.w3schools.com/html/html_colornames.asp\" title=\"HTML Color List - W3 School\">HTML color name list</a>."]
]
script [] [ rawText "PT.onLoad(PT.smallGroup.preferences.onPageLoad)" ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Group Preferences" |> Layout.standard vi "Group Preferences"

View File

@ -1,14 +1,13 @@
module PrayerTracker.Views.User module PrayerTracker.Views.User
open Giraffe.ViewEngine open Giraffe.ViewEngine
open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
/// View for the group assignment page /// View for the group assignment page
let assignGroups m groups curGroups ctx vi = let assignGroups m groups curGroups ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = sprintf "%s %A" m.UserName s["Assign Groups"] let pageTitle = sprintf "%s %A" m.UserName s["Assign Groups"]
form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ] input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ]
@ -24,9 +23,9 @@ let assignGroups m groups curGroups ctx vi =
let inputId = $"id-{grpId}" let inputId = $"id-{grpId}"
tr [] [ tr [] [
td [] [ td [] [
input [ _type "checkbox" input [ _type "checkbox"
_name (nameof m.SmallGroups) _name (nameof m.SmallGroups)
_id inputId _id inputId
_value grpId _value grpId
if List.contains grpId curGroups then _checked ] if List.contains grpId curGroups then _checked ]
] ]
@ -45,35 +44,36 @@ let assignGroups m groups curGroups ctx vi =
let changePassword ctx vi = let changePassword ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { OldPassword = ""; NewPassword = ""; NewPasswordConfirm = "" } let m = { OldPassword = ""; NewPassword = ""; NewPasswordConfirm = "" }
[ p [ _class "pt-center-text" ] [ [ p [ _class "pt-center-text" ] [
locStr s["To change your password, enter your current password in the specified box below, then enter your new password twice."] locStr s["To change your password, enter your current password in the specified box below, then enter your new password twice."]
] ]
form [ _action "/user/password/change" style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "]
_method "post" form [ _action "/user/password/change"
_onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')""" ] [ _method "post"
style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "] _onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')"""
csrfToken ctx Target.content ] [
div [ _class "pt-field-row" ] [ csrfToken ctx
div [ _class "pt-field" ] [ div [ _class "pt-field-row" ] [
label [ _for "oldPassword" ] [ locStr s["Current Password"] ] div [ _class "pt-field" ] [
input [ _type "password"; _name (nameof m.OldPassword); _id "oldPassword"; _required; _autofocus ] label [ _for "oldPassword" ] [ locStr s["Current Password"] ]
] input [ _type "password"; _name (nameof m.OldPassword); _id "oldPassword"; _required; _autofocus ]
] ]
div [ _class "pt-field-row" ] [ ]
div [ _class "pt-field" ] [ div [ _class "pt-field-row" ] [
label [ _for "newPassword" ] [ locStr s["New Password Twice"] ] div [ _class "pt-field" ] [
input [ _type "password"; _name (nameof m.NewPassword); _id "newPassword"; _required ] label [ _for "newPassword" ] [ locStr s["New Password Twice"] ]
] input [ _type "password"; _name (nameof m.NewPassword); _id "newPassword"; _required ]
div [ _class "pt-field" ] [ ]
label [] [ rawText "&nbsp;" ] div [ _class "pt-field" ] [
input [ _type "password"; _name (nameof m.NewPasswordConfirm); _id "newPasswordConfirm"; _required ] label [] [ rawText "&nbsp;" ]
] input [ _type "password"; _name (nameof m.NewPasswordConfirm); _id "newPasswordConfirm"; _required ]
] ]
div [ _class "pt-field-row" ] [ ]
submit [ _onclick "document.getElementById('newPasswordConfirm').setCustomValidity('')" ] "done" div [ _class "pt-field-row" ] [
s["Change Your Password"] submit [ _onclick "document.getElementById('newPasswordConfirm').setCustomValidity('')" ] "done"
] s["Change Your Password"]
] ]
]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Change Your Password" |> Layout.standard vi "Change Your Password"
@ -84,55 +84,58 @@ let edit (m : EditUser) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New User" else "Edit User" let pageTitle = if m.IsNew then "Add a New User" else "Edit User"
let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value
[ form [ _action "/user/edit/save"; _method "post"; _class "pt-center-columns" [ style [ _scoped ]
_onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" ] [
style [ _scoped ]
[ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ] [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ]
csrfToken ctx form [ _action "/user/edit/save"
input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] _method "post"
div [ _class "pt-field-row" ] [ _class "pt-center-columns"
div [ _class "pt-field" ] [ _onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')"""
label [ _for "firstName" ] [ locStr s["First Name"] ] Target.content ] [
input [ _type "text" csrfToken ctx
_name (nameof m.FirstName) input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
_id "firstName" div [ _class "pt-field-row" ] [
_value m.FirstName div [ _class "pt-field" ] [
_required label [ _for "firstName" ] [ locStr s["First Name"] ]
_autofocus ] input [ _type "text"
_name (nameof m.FirstName)
_id "firstName"
_value m.FirstName
_required
_autofocus ]
]
div [ _class "pt-field" ] [
label [ _for "lastName" ] [ locStr s["Last Name"] ]
input [ _type "text"; _name (nameof m.LastName); _id "lastName"; _value m.LastName; _required ]
]
div [ _class "pt-field" ] [
label [ _for "email" ] [ locStr s["E-mail Address"] ]
input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required ]
]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field-row" ] [
label [ _for "lastName" ] [ locStr s["Last Name"] ] div [ _class "pt-field" ] [
input [ _type "text"; _name (nameof m.LastName); _id "lastName"; _value m.LastName; _required ] label [ _for "password" ] [ locStr s["Password"] ]
input [ _type "password"; _name (nameof m.Password); _id "password"; _placeholder pwPlaceholder ]
]
div [ _class "pt-field" ] [
label [ _for "passwordConfirm" ] [ locStr s["Password Again"] ]
input [ _type "password"
_name (nameof m.PasswordConfirm)
_id "passwordConfirm"
_placeholder pwPlaceholder ]
]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-checkbox-field" ] [
label [ _for "email" ] [ locStr s["E-mail Address"] ] input [ _type "checkbox"
input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required ] _name (nameof m.IsAdmin)
_id "isAdmin"
_value "True"
if defaultArg m.IsAdmin false then _checked ]
label [ _for "isAdmin" ] [ locStr s["This user is a PrayerTracker administrator"] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save User"] ]
] ]
div [ _class "pt-field-row" ] [ script [] [ rawText $"PT.onLoad(PT.user.edit.onPageLoad({(string m.IsNew).ToLowerInvariant ()}))" ]
div [ _class "pt-field" ] [
label [ _for "password" ] [ locStr s["Password"] ]
input [ _type "password"; _name (nameof m.Password); _id "password"; _placeholder pwPlaceholder ]
]
div [ _class "pt-field" ] [
label [ _for "passwordConfirm" ] [ locStr s["Password Again"] ]
input [ _type "password"
_name (nameof m.PasswordConfirm)
_id "passwordConfirm"
_placeholder pwPlaceholder ]
]
]
div [ _class "pt-checkbox-field" ] [
input [ _type "checkbox"
_name (nameof m.IsAdmin)
_id "isAdmin"
_value "True"
if defaultArg m.IsAdmin false then _checked ]
label [ _for "isAdmin" ] [ locStr s["This user is a PrayerTracker administrator"] ]
]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save User"] ]
]
script [] [ rawText $"PT.onLoad(PT.user.edit.onPageLoad({(string m.IsNew).ToLowerInvariant ()}))" ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle
@ -141,7 +144,7 @@ let edit (m : EditUser) ctx vi =
/// View for the user log on page /// View for the user log on page
let logOn (m : UserLogOn) groups ctx vi = let logOn (m : UserLogOn) groups ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns"; Target.body ] [
style [ _scoped ] [ rawText "#email { width: 20rem; }" ] style [ _scoped ] [ rawText "#email { width: 20rem; }" ]
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ] input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ]
@ -152,20 +155,17 @@ let logOn (m : UserLogOn) groups ctx vi =
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "password" ] [ locStr s["Password"] ] label [ _for "password" ] [ locStr s["Password"] ]
input [ _type "password" input [ _type "password"
_name (nameof m.Password) _name (nameof m.Password)
_id "password" _id "password"
_required; _placeholder $"""({s["Case-Sensitive"].Value.ToLower ()})"""
_placeholder (sprintf "(%s)" (s["Case-Sensitive"].Value.ToLower ())) ] _required ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq { seq { "", selectDefault s["Select Group"].Value; yield! groups }
"", selectDefault s["Select Group"].Value
yield! groups
}
|> selectList (nameof m.SmallGroupId) "" [ _required ] |> selectList (nameof m.SmallGroupId) "" [ _required ]
] ]
] ]
@ -173,7 +173,7 @@ let logOn (m : UserLogOn) groups ctx vi =
input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ] input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ]
label [ _for "rememberMe" ] [ locStr s["Remember Me"] ] label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
br [] br []
small [] [ em [] [ rawText "("; str (s["Requires Cookies"].Value.ToLower ()); rawText ")" ] ] small [] [ em [] [ str $"""({s["Requires Cookies"].Value.ToLower ()})""" ] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ] div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ]
] ]
@ -182,6 +182,8 @@ let logOn (m : UserLogOn) groups ctx vi =
|> Layout.standard vi "User Log On" |> Layout.standard vi "User Log On"
open PrayerTracker.Entities
/// View for the user maintenance page /// View for the user maintenance page
let maintain (users : User list) ctx vi = let maintain (users : User list) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
@ -206,12 +208,14 @@ let maintain (users : User list) ctx vi =
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ icon "edit" ] a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ icon "edit" ]
a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ] a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ] [
[ icon "group" ] icon "group"
a [ _href delAction ]
_title s["Delete This User"].Value a [ _href delAction
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _title s["Delete This User"].Value
[ icon "delete_forever" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
] ]
td [] [ str user.fullName ] td [] [ str user.fullName ]
td [ _class "pt-center-text" ] [ td [ _class "pt-center-text" ] [
@ -219,17 +223,18 @@ let maintain (users : User list) ctx vi =
] ]
]) ])
|> tbody [] |> tbody []
] ]
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/user/{emptyGuid}/edit"; _title s["Add a New User"].Value ] a [ _href $"/user/{emptyGuid}/edit"; _title s["Add a New User"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New User"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New User"]
br [] ]
br [] br []
] br []
tableSummary users.Length s ]
usrTbl tableSummary users.Length s
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] usrTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "Maintain Users" |> Layout.standard vi "Maintain Users"

View File

@ -103,6 +103,18 @@ module UserMessage =
Description = None Description = None
} }
/// The template with which the content will be rendered
type LayoutType =
/// A full page load
| FullPage
/// A response that will provide a new body tag
| PartialPage
/// A response that will replace the page content
| ContentOnly
open System open System
@ -132,6 +144,9 @@ type AppViewInfo =
/// The currently logged on small group, if there is one /// The currently logged on small group, if there is one
Group : SmallGroup option Group : SmallGroup option
/// The layout with which the content will be rendered
Layout : LayoutType
} }
/// Support for the AppViewInfo type /// Support for the AppViewInfo type
@ -147,6 +162,7 @@ module AppViewInfo =
RequestStart = DateTime.Now.Ticks RequestStart = DateTime.Now.Ticks
User = None User = None
Group = None Group = None
Layout = FullPage
} }

View File

@ -18,7 +18,7 @@ let private findStats (db : AppDbContext) churchId = task {
/// POST /church/[church-id]/delete /// POST /church/[church-id]/delete
let delete churchId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let delete churchId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.db.TryChurchById churchId with match! ctx.db.TryChurchById churchId with
| Some church -> | Some church ->
let! _, stats = findStats ctx.db churchId let! _, stats = findStats ctx.db churchId
@ -66,7 +66,7 @@ let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
/// POST /church/save /// POST /church/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditChurch> () with match! ctx.TryBindFormAsync<EditChurch> () with
| Ok m -> | Ok m ->
let! church = let! church =

View File

@ -2,20 +2,7 @@
[<AutoOpen>] [<AutoOpen>]
module PrayerTracker.Handlers.CommonFunctions module PrayerTracker.Handlers.CommonFunctions
open System
open System.Net
open System.Reflection
open System.Threading.Tasks
open Giraffe
open Microsoft.AspNetCore.Antiforgery
open Microsoft.AspNetCore.Html
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Extensions
open Microsoft.AspNetCore.Mvc.Rendering open Microsoft.AspNetCore.Mvc.Rendering
open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Cookies
open PrayerTracker.ViewModels
/// Create a select list from an enumeration /// Create a select list from an enumeration
let toSelectList<'T> valFunc textFunc withDefault emptyText (items : 'T seq) = let toSelectList<'T> valFunc textFunc withDefault emptyText (items : 'T seq) =
@ -38,7 +25,7 @@ let toSelectListWithDefault<'T> valFunc textFunc (items : 'T seq) =
/// The version of PrayerTracker /// The version of PrayerTracker
let appVersion = let appVersion =
let v = Assembly.GetExecutingAssembly().GetName().Version let v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version
#if (DEBUG) #if (DEBUG)
$"v{v}" $"v{v}"
#else #else
@ -53,6 +40,10 @@ let appVersion =
|> String.concat "" |> String.concat ""
#endif #endif
open Microsoft.AspNetCore.Http
open PrayerTracker
/// The currently signed-in user (will raise if none exists) /// The currently signed-in user (will raise if none exists)
let currentUser (ctx : HttpContext) = let currentUser (ctx : HttpContext) =
match ctx.Session.user with Some u -> u | None -> nullArg "User" match ctx.Session.user with Some u -> u | None -> nullArg "User"
@ -61,6 +52,12 @@ let currentUser (ctx : HttpContext) =
let currentGroup (ctx : HttpContext) = let currentGroup (ctx : HttpContext) =
match ctx.Session.smallGroup with Some g -> g | None -> nullArg "SmallGroup" match ctx.Session.smallGroup with Some g -> g | None -> nullArg "SmallGroup"
open System
open Giraffe
open PrayerTracker.Cookies
open PrayerTracker.ViewModels
/// Create the common view information heading /// Create the common view information heading
let viewInfo (ctx : HttpContext) startTicks = let viewInfo (ctx : HttpContext) startTicks =
let msg = let msg =
@ -84,12 +81,18 @@ let viewInfo (ctx : HttpContext) startTicks =
CookieOptions (Expires = Nullable<DateTimeOffset> (DateTimeOffset (DateTime timeout.Until)), CookieOptions (Expires = Nullable<DateTimeOffset> (DateTimeOffset (DateTime timeout.Until)),
HttpOnly = true)) HttpOnly = true))
| None -> () | None -> ()
let layout =
match ctx.TryGetRequestHeader "X-Target" with
| Some hdr when hdr = "#pt-body" -> ContentOnly
| Some _ -> PartialPage
| None -> FullPage
{ AppViewInfo.fresh with { AppViewInfo.fresh with
Version = appVersion Version = appVersion
Messages = msg Messages = msg
RequestStart = startTicks RequestStart = startTicks
User = ctx.Session.user User = ctx.Session.user
Group = ctx.Session.smallGroup Group = ctx.Session.smallGroup
Layout = layout
} }
/// The view is the last parameter, so it can be composed /// The view is the last parameter, so it can be composed
@ -107,23 +110,24 @@ let fourOhFour next (ctx : HttpContext) =
ctx.SetStatusCode 404 ctx.SetStatusCode 404
text "Not Found" next ctx text "Not Found" next ctx
/// Handler to validate CSRF prevention token /// Handler to validate CSRF prevention token
let validateCSRF : HttpHandler = fun next ctx -> task { let validateCsrf : HttpHandler = fun next ctx -> task {
match! (ctx.GetService<IAntiforgery> ()).IsRequestValidAsync ctx with match! (ctx.GetService<Microsoft.AspNetCore.Antiforgery.IAntiforgery> ()).IsRequestValidAsync ctx with
| true -> return! next ctx | true -> return! next ctx
| false -> | false -> return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") earlyReturn ctx
return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") (fun _ -> Task.FromResult None) ctx
} }
/// Add a message to the session /// Add a message to the session
let addUserMessage (ctx : HttpContext) msg = let addUserMessage (ctx : HttpContext) msg =
ctx.Session.messages <- msg :: ctx.Session.messages ctx.Session.messages <- msg :: ctx.Session.messages
open Microsoft.AspNetCore.Html
open Microsoft.Extensions.Localization
/// Convert a localized string to an HTML string /// Convert a localized string to an HTML string
let htmlLocString (x : LocalizedString) = let htmlLocString (x : LocalizedString) =
(WebUtility.HtmlEncode >> HtmlString) x.Value (System.Net.WebUtility.HtmlEncode >> HtmlString) x.Value
let htmlString (x : LocalizedString) = let htmlString (x : LocalizedString) =
HtmlString x.Value HtmlString x.Value
@ -157,6 +161,8 @@ type AccessLevel =
| Public | Public
open Microsoft.AspNetCore.Http.Extensions
/// Require the given access role (also refreshes "Remember Me" user and group logons) /// Require the given access role (also refreshes "Remember Me" user and group logons)
let requireAccess level : HttpHandler = let requireAccess level : HttpHandler =

View File

@ -94,7 +94,7 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
/// POST /prayer-request/[request-id]/delete /// POST /prayer-request/[request-id]/delete
let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! findRequest ctx reqId with match! findRequest ctx reqId with
| Ok req -> | Ok req ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -215,7 +215,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
/// POST /prayer-request/save /// POST /prayer-request/save
let save : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditRequest> () with match! ctx.TryBindFormAsync<EditRequest> () with
| Ok m -> | Ok m ->
let! req = let! req =

View File

@ -26,6 +26,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>

View File

@ -27,7 +27,7 @@ let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /small-group/[group-id]/delete /// POST /small-group/[group-id]/delete
let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
match! ctx.db.TryGroupById groupId with match! ctx.db.TryGroupById groupId with
| Some grp -> | Some grp ->
@ -44,7 +44,7 @@ let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=>
/// POST /small-group/member/[member-id]/delete /// POST /small-group/member/[member-id]/delete
let deleteMember memberId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let deleteMember memberId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
match! ctx.db.TryMemberById memberId with match! ctx.db.TryMemberById memberId with
| Some mbr when mbr.smallGroupId = (currentGroup ctx).smallGroupId -> | Some mbr when mbr.smallGroupId = (currentGroup ctx).smallGroupId ->
@ -113,7 +113,7 @@ let logOn (groupId : SmallGroupId option) : HttpHandler = requireAccess [ Access
/// POST /small-group/log-on/submit /// POST /small-group/log-on/submit
let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSRF >=> fun next ctx -> task { let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<GroupLogOn> () with match! ctx.TryBindFormAsync<GroupLogOn> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -193,7 +193,7 @@ let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task
/// POST /small-group/save /// POST /small-group/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditSmallGroup> () with match! ctx.TryBindFormAsync<EditSmallGroup> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -218,7 +218,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
/// POST /small-group/member/save /// POST /small-group/member/save
let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditMember> () with match! ctx.TryBindFormAsync<EditMember> () with
| Ok m -> | Ok m ->
let grp = currentGroup ctx let grp = currentGroup ctx
@ -246,7 +246,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun n
/// POST /small-group/preferences/save /// POST /small-group/preferences/save
let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditPreferences> () with match! ctx.TryBindFormAsync<EditPreferences> () with
| Ok m -> | Ok m ->
// Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works, // Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works,
@ -268,7 +268,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
/// POST /small-group/announcement/send /// POST /small-group/announcement/send
let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let startTicks = DateTime.Now.Ticks let startTicks = DateTime.Now.Ticks
match! ctx.TryBindFormAsync<Announcement> () with match! ctx.TryBindFormAsync<Announcement> () with
| Ok m -> | Ok m ->

View File

@ -44,7 +44,7 @@ let private findUserByPassword m (db : AppDbContext) = task {
/// POST /user/password/change /// POST /user/password/change
let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<ChangePassword> () with match! ctx.TryBindFormAsync<ChangePassword> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -81,7 +81,7 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> f
/// POST /user/[user-id]/delete /// POST /user/[user-id]/delete
let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.db.TryUserById userId with match! ctx.db.TryUserById userId with
| Some user -> | Some user ->
ctx.db.RemoveEntry user ctx.db.RemoveEntry user
@ -94,7 +94,7 @@ let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> f
/// POST /user/log-on /// POST /user/log-on
let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSRF >=> fun next ctx -> task { let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<UserLogOn> () with match! ctx.TryBindFormAsync<UserLogOn> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -192,7 +192,7 @@ let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /user/save /// POST /user/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditUser> () with match! ctx.TryBindFormAsync<EditUser> () with
| Ok m -> | Ok m ->
let! user = let! user =
@ -235,7 +235,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
/// POST /user/small-groups/save /// POST /user/small-groups/save
let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<AssignGroups> () with match! ctx.TryBindFormAsync<AssignGroups> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()