From f66dd76dfb59b89f6b1cdbd6dcc6c5c73abf082f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 31 Jul 2022 21:25:44 -0400 Subject: [PATCH] Add style/onload to render context (#36) --- src/PrayerTracker.UI/Church.fs | 101 ++-- src/PrayerTracker.UI/CommonFunctions.fs | 19 + src/PrayerTracker.UI/Home.fs | 20 +- src/PrayerTracker.UI/Layout.fs | 6 + src/PrayerTracker.UI/PrayerRequest.fs | 248 ++++----- src/PrayerTracker.UI/SmallGroup.fs | 655 ++++++++++++------------ src/PrayerTracker.UI/User.fs | 208 ++++---- src/PrayerTracker.UI/ViewModels.fs | 35 +- src/PrayerTracker/wwwroot/js/app.js | 2 +- 9 files changed, 683 insertions(+), 611 deletions(-) diff --git a/src/PrayerTracker.UI/Church.fs b/src/PrayerTracker.UI/Church.fs index 3149a5d..a3e88c1 100644 --- a/src/PrayerTracker.UI/Church.fs +++ b/src/PrayerTracker.UI/Church.fs @@ -5,57 +5,62 @@ open PrayerTracker.Entities open PrayerTracker.ViewModels /// View for the church edit page -let edit (m : EditChurch) ctx vi = - let pageTitle = if m.IsNew then "Add a New Church" else "Edit Church" +let edit (model : EditChurch) ctx viewInfo = + let pageTitle = if model.IsNew then "Add a New Church" else "Edit Church" let s = I18N.localizer.Force () - [ form [ _action "/church/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ - style [ _scoped ] [ - 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" ] [ - 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-row" ] [ - div [ _class "pt-checkbox-field" ] [ - input [ _type "checkbox" - _name (nameof m.HasInterface) - _id "hasInterface" - _value "True" - if defaultArg m.HasInterface false then _checked ] - 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" ] [ - 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"] ] + let vi = + viewInfo + |> AppViewInfo.withScopedStyles [ + "#name { width: 20rem; }" + "#city { width: 10rem; }" + "#st { width: 3rem; }" + "#interfaceAddress { width: 30rem; }" ] - script [] [ rawText "PT.onLoad(PT.church.edit.onPageLoad)" ] + |> AppViewInfo.withOnLoadScript "PT.church.edit.onPageLoad" + form [ _action "/church/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ + csrfToken ctx + input [ _type "hidden"; _name (nameof model.ChurchId); _value (flatGuid model.ChurchId) ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "name" ] [ locStr s["Church Name"] ] + input [ _type "text"; _name (nameof model.Name); _id "name"; _required; _autofocus; _value model.Name ] + ] + div [ _inputField ] [ + label [ _for "City"] [ locStr s["City"] ] + input [ _type "text"; _name (nameof model.City); _id "city"; _required; _value model.City ] + ] + div [ _inputField ] [ + label [ _for "state" ] [ locStr s["State or Province"] ] + input [ _type "text" + _name (nameof model.State) + _id "state" + _minlength "2"; _maxlength "2" + _value model.State + _required ] + ] + ] + div [ _fieldRow ] [ + div [ _checkboxField ] [ + input [ _type "checkbox" + _name (nameof model.HasInterface) + _id "hasInterface" + _value "True" + if defaultArg model.HasInterface false then _checked ] + label [ _for "hasInterface" ] [ locStr s["Has an interface with Virtual Prayer Room"] ] + ] + ] + div [ _fieldRowWith [ "pt-fadeable" ]; _id "divInterfaceAddress" ] [ + div [ _inputField ] [ + label [ _for "interfaceAddress" ] [ locStr s["VPR Interface URL"] ] + input [ _type "url" + _name (nameof model.InterfaceAddress) + _id "interfaceAddress"; + _value (defaultArg model.InterfaceAddress "") ] + ] + ] + div [ _fieldRow ] [ submit [] "save" s["Save Church"] ] ] + |> List.singleton |> Layout.Content.standard |> Layout.standard vi pageTitle diff --git a/src/PrayerTracker.UI/CommonFunctions.fs b/src/PrayerTracker.UI/CommonFunctions.fs index fee6843..40a3d1d 100644 --- a/src/PrayerTracker.UI/CommonFunctions.fs +++ b/src/PrayerTracker.UI/CommonFunctions.fs @@ -119,6 +119,25 @@ let _onsubmit = attr "onsubmit" /// A "rel='noopener'" attribute let _relNoOpener = _rel "noopener" +/// A class attribute that designates a row of fields, with the additional classes passed +let _fieldRowWith classes = + let extraClasses = if List.isEmpty classes then "" else $""" {classes |> String.concat " "}""" + _class $"pt-field-row{extraClasses}" + +/// The class that designates a row of fields +let _fieldRow = _fieldRowWith [] + +/// A class attribute that designates an input field, with the additional classes passed +let _inputFieldWith classes = + let extraClasses = if List.isEmpty classes then "" else $""" {classes |> String.concat " "}""" + _class $"pt-field{extraClasses}" + +/// The class that designates an input field / label pair +let _inputField = _inputFieldWith [] + +/// The class that designates a checkbox / label pair +let _checkboxField = _class "pt-checkbox-field" + /// The name this function used to have when the view engine was part of Giraffe let renderHtmlNode = RenderView.AsString.htmlNode diff --git a/src/PrayerTracker.UI/Home.fs b/src/PrayerTracker.UI/Home.fs index c7fa794..09a3b57 100644 --- a/src/PrayerTracker.UI/Home.fs +++ b/src/PrayerTracker.UI/Home.fs @@ -6,7 +6,7 @@ open Giraffe.ViewEngine open PrayerTracker.ViewModels /// The error page -let error code vi = +let error code viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Home/Error" use sw = new StringWriter () @@ -37,7 +37,7 @@ let error code vi = _alt $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}""" _title $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}""" _style "vertical-align:text-bottom;" ] - str vi.Version + str viewInfo.Version ] ] |> div [] @@ -45,7 +45,7 @@ let error code vi = /// The home page -let index vi = +let index viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Home/Index" use sw = new StringWriter () @@ -118,11 +118,11 @@ let index vi = ] ] |> Layout.Content.standard - |> Layout.standard vi "Welcome!" + |> Layout.standard viewInfo "Welcome!" /// Privacy Policy page -let privacyPolicy vi = +let privacyPolicy viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Home/PrivacyPolicy" use sw = new StringWriter () @@ -190,11 +190,11 @@ let privacyPolicy vi = ] ] |> Layout.Content.standard - |> Layout.standard vi "Privacy Policy" + |> Layout.standard viewInfo "Privacy Policy" /// Terms of Service page -let termsOfService vi = +let termsOfService viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Home/TermsOfService" use sw = new StringWriter () @@ -237,11 +237,11 @@ let termsOfService vi = p [] [ raw l["You may also wish to review our {0} to learn how we handle your data.", ppLink] ] ] |> Layout.Content.standard - |> Layout.standard vi "Terms of Service" + |> Layout.standard viewInfo "Terms of Service" /// View for unauthorized page -let unauthorized vi = +let unauthorized viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Home/Unauthorized" use sw = new StringWriter () @@ -256,4 +256,4 @@ let unauthorized vi = ] ] |> Layout.Content.standard - |> Layout.standard vi "Unauthorized Access" + |> Layout.standard viewInfo "Unauthorized Access" diff --git a/src/PrayerTracker.UI/Layout.fs b/src/PrayerTracker.UI/Layout.fs index 25ae505..e17d9d6 100644 --- a/src/PrayerTracker.UI/Layout.fs +++ b/src/PrayerTracker.UI/Layout.fs @@ -293,10 +293,16 @@ let private contentSection viewInfo title (content : XmlNode) = [ Navigation.identity viewInfo renderPageTitle viewInfo title yield! messages viewInfo + match viewInfo.ScopedStyle with + | [] -> () + | styles -> style [ _scoped ] (styles |> List.map (fun it -> rawText $"{it};")) content htmlFooter viewInfo for jsFile in viewInfo.Script do script [ _src $"/js/{jsFile}.js" ] [] + match viewInfo.OnLoadScript with + | Some onLoad -> script [] [ rawText $"PT.onLoad({onLoad}" ] + | None -> () ] /// The HTML head element for partial responses diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index 13a577d..595e465 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -11,79 +11,80 @@ open PrayerTracker.Entities open PrayerTracker.ViewModels /// View for the prayer request edit page -let edit (m : EditRequest) today ctx vi = +let edit (model : EditRequest) today ctx viewInfo = let s = I18N.localizer.Force () - let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request" - [ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ - csrfToken ctx - input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ] - 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 ] + let pageTitle = if model.IsNew then "Add a New Request" else "Edit Request" + let vi = AppViewInfo.withOnLoadScript "PT.initCKEditor" viewInfo + form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ + csrfToken ctx + input [ _type "hidden"; _name (nameof model.RequestId); _value (flatGuid model.RequestId) ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.RequestType) ] [ locStr s["Request Type"] ] + ReferenceList.requestTypeList s + |> Seq.ofList + |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) + |> selectList (nameof model.RequestType) model.RequestType [ _required; _autofocus ] + ] + div [ _inputField ] [ + label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ] + input [ _type "text" + _id "requestor" + _name (nameof model.Requestor) + _value (defaultArg model.Requestor "") ] + ] + if model.IsNew then + div [ _inputField ] [ + label [ _for "enteredDate" ] [ locStr s["Date"] ] + input [ _type "date"; _name (nameof model.EnteredDate); _id "enteredDate"; _placeholder today ] ] - div [ _class "pt-field" ] [ - label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ] - input [ _type "text" - _id "requestor" - _name (nameof m.Requestor) - _value (defaultArg m.Requestor "") ] - ] - 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 + // TODO: do these need to be nested like this? + div [ _inputField ] [ + div [ _checkboxField ] [ + br [] + input [ _type "checkbox" + _name (nameof model.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." ] ] ] - 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 "     " - ]) - |> div [ _class "pt-center-text" ] ] - ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field pt-editor" ] [ - label [ _for "text" ] [ locStr s["Request"] ] - textarea [ _name (nameof m.Text); _id "text" ] [ str m.Text ] - ] - ] - div [ _class "pt-field-row" ] [ submit [] "save" s["Save Request"] ] ] - script [] [ rawText "PT.onLoad(PT.initCKEditor)" ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [] [ locStr s["Expiration"] ] + ReferenceList.expirationList s (not model.IsNew) + |> List.map (fun exp -> + let radioId = $"expiration_{fst exp}" + span [ _class "text-nowrap" ] [ + radio (nameof model.Expiration) radioId (fst exp) model.Expiration + label [ _for radioId ] [ locStr (snd exp) ] + rawText "     " + ]) + |> div [ _class "pt-center-text" ] + ] + ] + div [ _fieldRow ] [ + div [ _inputFieldWith [ "pt-editor" ] ] [ + label [ _for "text" ] [ locStr s["Request"] ] + textarea [ _name (nameof model.Text); _id "text" ] [ str model.Text ] + ] + ] + div [ _fieldRow ] [ submit [] "save" s["Save Request"] ] ] + |> List.singleton |> Layout.Content.standard |> Layout.standard vi pageTitle /// View for the request e-mail results page -let email m vi = +let email model viewInfo = let s = I18N.localizer.Force () - let pageTitle = $"""{s["Prayer Requests"].Value} • {m.SmallGroup.name}""" - let prefs = m.SmallGroup.preferences - let addresses = m.Recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>") |> String.concat ", " + let pageTitle = $"""{s["Prayer Requests"].Value} • {model.SmallGroup.name}""" + let prefs = model.SmallGroup.preferences + let addresses = model.Recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>") |> String.concat ", " [ 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"] rawText ":" @@ -91,27 +92,27 @@ let email m vi = small [] [ str addresses ] ] 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 (model.AsHtml s) ] br [] br [] 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 (model.AsText s) ] ] ] |> Layout.Content.standard - |> Layout.standard vi pageTitle + |> Layout.standard viewInfo pageTitle /// View for a small group's public prayer request list -let list (m : RequestList) vi = +let list (model : RequestList) viewInfo = [ br [] - I18N.localizer.Force () |> (m.AsHtml >> rawText) + I18N.localizer.Force () |> (model.AsHtml >> rawText) ] |> Layout.Content.standard - |> Layout.standard vi "View Request List" + |> Layout.standard viewInfo "View Request List" /// View for the prayer request lists page -let lists (groups : SmallGroup list) vi = +let lists (groups : SmallGroup list) viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Requests/Lists" use sw = new StringWriter () @@ -154,17 +155,17 @@ let lists (groups : SmallGroup list) vi = ] ] |> Layout.Content.standard - |> Layout.standard vi "Request Lists" + |> Layout.standard viewInfo "Request Lists" /// View for the prayer request maintenance page -let maintain (m : MaintainRequests) (ctx : HttpContext) vi = +let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Requests/Maintain" use sw = new StringWriter () let raw = rawLocText sw - let now = m.SmallGroup.localDateNow (ctx.GetService ()) - let prefs = m.SmallGroup.preferences + let now = model.SmallGroup.localDateNow (ctx.GetService ()) + let prefs = model.SmallGroup.preferences let types = ReferenceList.requestTypeList s |> Map.ofList let updReq (req : PrayerRequest) = if req.updateRequired now prefs.daysToExpire prefs.longTermUpdateWeeks then "pt-request-update" else "" @@ -173,7 +174,7 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi = _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 let requests = - m.Requests + model.Requests |> List.map (fun req -> let reqId = flatGuid req.prayerRequestId let reqText = htmlToPlainText req.text @@ -228,7 +229,7 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi = a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [ icon "list"; rawText "  "; locStr s["View Prayer Request List"] ] - match m.SearchTerm with + match model.SearchTerm with | Some _ -> rawText "       " a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] [ @@ -240,7 +241,7 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi = input [ _type "text" _name "search" _placeholder l["Search requests..."].Value - _value (defaultArg m.SearchTerm "") + _value (defaultArg model.SearchTerm "") ] space submit [] "search" s["Search"] @@ -264,22 +265,22 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi = ] div [ _class "pt-center-text" ] [ br [] - match m.OnlyActive with + match model.OnlyActive with | Some true -> raw l["Inactive requests are currently not shown"] br [] a [ _href "/prayer-requests/inactive" ] [ raw l["Show Inactive Requests"] ] | _ -> - if defaultArg m.OnlyActive false then + if defaultArg model.OnlyActive false then raw l["Inactive requests are currently shown"] br [] a [ _href "/prayer-requests" ] [ raw l["Do Not Show Inactive Requests"] ] br [] br [] - let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ] - let pg = defaultArg m.PageNbr 1 + let search = [ match model.SearchTerm with Some s -> "search", s | None -> () ] + let pg = defaultArg model.PageNbr 1 let url = - match m.OnlyActive with Some true | None -> "" | _ -> "/inactive" + match model.OnlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/prayer-requests%s" match pg with | 1 -> () @@ -288,7 +289,7 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi = 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 "     " - match requests.Length = m.SmallGroup.preferences.pageSize with + match requests.Length = model.SmallGroup.preferences.pageSize with | true -> a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] [ raw l["Next Page"]; space; icon "keyboard_arrow_right" @@ -298,19 +299,19 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi = form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] |> Layout.Content.wide - |> Layout.standard vi (match m.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests") + |> Layout.standard viewInfo (match model.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests") /// View for the printable prayer request list -let print m version = +let print model version = let s = I18N.localizer.Force () - let pageTitle = $"""{s["Prayer Requests"].Value} • {m.SmallGroup.name}""" + let pageTitle = $"""{s["Prayer Requests"].Value} • {model.SmallGroup.name}""" let imgAlt = $"""{s["PrayerTracker"].Value} {s["from Bit Badger Solutions"].Value}""" article [] [ - rawText (m.AsHtml s) + rawText (model.AsHtml s) br [] hr [] - div [ _style $"font-size:70%%;font-family:{m.SmallGroup.preferences.listFonts};" ] [ + div [ _style $"font-size:70%%;font-family:{model.SmallGroup.preferences.listFonts};" ] [ img [ _src $"""/img/{s["footer_en"].Value}.png""" _style "vertical-align:text-bottom;" _alt imgAlt @@ -323,45 +324,44 @@ let print m version = /// View for the prayer request list -let view m vi = +let view model viewInfo = let s = I18N.localizer.Force () - let pageTitle = $"""{s["Prayer Requests"].Value} • {m.SmallGroup.name}""" + let pageTitle = $"""{s["Prayer Requests"].Value} • {model.SmallGroup.name}""" let spacer = rawText "       " - let dtString = m.Date.ToString "yyyy-MM-dd" - [ div [ _class "pt-center-text" ] [ - br [] - a [ _class "pt-icon-link" - _href $"/prayer-requests/print/{dtString}" - _target "_blank" - _title s["View Printable"].Value ] [ - icon "print"; rawText "  "; locStr s["View Printable"] - ] - if m.CanEmail then - spacer - if m.Date.DayOfWeek <> DayOfWeek.Sunday then - let rec findSunday (date : DateTime) = - if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.) - let sunday = findSunday m.Date - a [ _class "pt-icon-link" - _href $"""/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}""" - _title s["List for Next Sunday"].Value ] [ - icon "update"; rawText "  "; locStr s["List for Next Sunday"] - ] - spacer - 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" - _href $"/prayer-requests/email/{dtString}" - _title s["Send via E-mail"].Value - _onclick $"return PT.requests.view.promptBeforeEmail('{emailPrompt}')" ] [ - icon "mail_outline"; rawText "  "; 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 "  "; locStr s["Maintain Prayer Requests"] - ] - ] + let dtString = model.Date.ToString "yyyy-MM-dd" + div [ _class "pt-center-text" ] [ br [] - rawText (m.AsHtml s) + a [ _class "pt-icon-link" + _href $"/prayer-requests/print/{dtString}" + _target "_blank" + _title s["View Printable"].Value ] [ + icon "print"; rawText "  "; locStr s["View Printable"] + ] + if model.CanEmail then + spacer + if model.Date.DayOfWeek <> DayOfWeek.Sunday then + let rec findSunday (date : DateTime) = + if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.) + let sunday = findSunday model.Date + a [ _class "pt-icon-link" + _href $"""/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}""" + _title s["List for Next Sunday"].Value ] [ + icon "update"; rawText "  "; locStr s["List for Next Sunday"] + ] + spacer + 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" + _href $"/prayer-requests/email/{dtString}" + _title s["Send via E-mail"].Value + _onclick $"return PT.requests.view.promptBeforeEmail('{emailPrompt}')" ] [ + icon "mail_outline"; rawText "  "; 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 "  "; locStr s["Maintain Prayer Requests"] + ] ] + |> List.singleton + |> List.append [ br []; rawText (model.AsHtml s) ] |> Layout.Content.standard - |> Layout.standard vi pageTitle + |> Layout.standard viewInfo pageTitle diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index 2863e06..551ea86 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -6,125 +6,125 @@ open PrayerTracker.Entities open PrayerTracker.ViewModels /// View for the announcement page -let announcement isAdmin ctx vi = +let announcement isAdmin ctx viewInfo = let s = I18N.localizer.Force () - let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None } + let model = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None } let reqTypes = ReferenceList.requestTypeList s - [ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns"; Target.content ] [ - 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 ] [] - ] + let vi = AppViewInfo.withOnLoadScript "PT.smallGroup.announcement.onPageLoad" viewInfo + form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns"; Target.content ] [ + csrfToken ctx + div [ _fieldRow ] [ + div [ _inputFieldWith [ "pt-editor" ] ] [ + label [ _for "text" ] [ locStr s["Announcement Text"] ] + textarea [ _name (nameof model.Text); _id "text"; _autofocus ] [] ] - if isAdmin then - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [] [ locStr s["Send Announcement to"]; rawText ":" ] - div [ _class "pt-center-text" ] [ - radio (nameof m.SendToClass) "sendY" "Y" "Y" - label [ _for "sendY" ] [ locStr s["This Group"]; rawText "     " ] - radio (nameof m.SendToClass) "sendN" "N" "Y" - label [ _for "sendN" ] [ locStr s["All {0} Users", s["PrayerTracker"]] ] - ] + ] + if isAdmin then + div [ _fieldRow ] [ + div [ _inputField ] [ + label [] [ locStr s["Send Announcement to"]; rawText ":" ] + div [ _class "pt-center-text" ] [ + radio (nameof model.SendToClass) "sendY" "Y" "Y" + label [ _for "sendY" ] [ locStr s["This Group"]; rawText "     " ] + radio (nameof model.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" ] [ - label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ] - reqTypes - |> Seq.ofList - |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) - |> selectList (nameof m.RequestType) Announcement.code [] - ] + else input [ _type "hidden"; _name (nameof model.SendToClass); _value "Y" ] + div [ _fieldRowWith [ "pt-fadeable"; "pt-shown" ]; _id "divAddToList" ] [ + div [ _checkboxField ] [ + input [ _type "checkbox"; _name (nameof model.AddToRequestList); _id "addToRequestList"; _value "True" ] + label [ _for "addToRequestList" ] [ locStr s["Add to Request List"] ] ] - div [ _class "pt-field-row" ] [ submit [] "send" s["Send Announcement"] ] ] - script [] [ rawText "PT.onLoad(PT.smallGroup.announcement.onPageLoad)" ] + div [ _fieldRowWith [ "pt-fadeable" ]; _id "divCategory" ] [ + div [ _inputField ] [ + label [ _for (nameof model.RequestType) ] [ locStr s["Request Type"] ] + reqTypes + |> Seq.ofList + |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) + |> selectList (nameof model.RequestType) Announcement.code [] + ] + ] + div [ _fieldRow ] [ submit [] "send" s["Send Announcement"] ] ] + |> List.singleton |> Layout.Content.standard |> Layout.standard vi "Send Announcement" /// View for once an announcement has been sent -let announcementSent (m : Announcement) vi = +let announcementSent (model : Announcement) viewInfo = let s = I18N.localizer.Force () [ span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] - div [ _class "pt-email-canvas" ] [ rawText m.Text ] + div [ _class "pt-email-canvas" ] [ rawText model.Text ] br [] br [] 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 model.PlainText ] ] ] |> Layout.Content.standard - |> Layout.standard vi "Announcement Sent" + |> Layout.standard viewInfo "Announcement Sent" /// View for the small group add/edit page -let edit (m : EditSmallGroup) (churches : Church list) ctx vi = +let edit (model : EditSmallGroup) (churches : Church list) ctx viewInfo = let s = I18N.localizer.Force () - let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group" + let pageTitle = if model.IsNew then "Add a New Group" else "Edit Group" form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ csrfToken ctx - input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ + input [ _type "hidden"; _name (nameof model.SmallGroupId); _value (flatGuid model.SmallGroupId) ] + div [ _fieldRow ] [ + div [ _inputField ] [ label [ _for "name" ] [ locStr s["Group Name"] ] - input [ _type "text"; _name (nameof m.Name); _id "name"; _value m.Name; _required; _autofocus ] + input [ _type "text"; _name (nameof model.Name); _id "name"; _value model.Name; _required; _autofocus ] ] ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [ _for (nameof m.ChurchId) ] [ locStr s["Church"] ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.ChurchId) ] [ locStr s["Church"] ] seq { - "", selectDefault s["Select Church"].Value - yield! churches |> List.map (fun c -> flatGuid c.churchId, c.name) - } - |> selectList (nameof m.ChurchId) (flatGuid m.ChurchId) [ _required ] + "", selectDefault s["Select Church"].Value + yield! churches |> List.map (fun c -> flatGuid c.churchId, c.name) + } + |> selectList (nameof model.ChurchId) (flatGuid model.ChurchId) [ _required ] ] ] - div [ _class "pt-field-row" ] [ submit [] "save" s["Save Group"] ] + div [ _fieldRow ] [ submit [] "save" s["Save Group"] ] ] |> List.singleton |> Layout.Content.standard - |> Layout.standard vi pageTitle + |> Layout.standard viewInfo pageTitle /// View for the member edit page -let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi = +let editMember (model : EditMember) (types : (string * LocalizedString) seq) ctx viewInfo = let s = I18N.localizer.Force () - let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member" + let pageTitle = if model.IsNew then "Add a New Group Member" else "Edit Group Member" + let vi = AppViewInfo.withScopedStyles [ "#name { width: 15rem; }"; "#email { width: 20rem; }" ] viewInfo form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ - style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ] csrfToken ctx - input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ + input [ _type "hidden"; _name (nameof model.MemberId); _value (flatGuid model.MemberId) ] + div [ _fieldRow ] [ + div [ _inputField ] [ label [ _for "name" ] [ locStr s["Member Name"] ] - input [ _type "text"; _name (nameof m.Name); _id "name"; _required; _autofocus; _value m.Name ] + input [ _type "text"; _name (nameof model.Name); _id "name"; _required; _autofocus; _value model.Name ] ] - div [ _class "pt-field" ] [ + div [ _inputField ] [ label [ _for "email" ] [ locStr s["E-mail Address"] ] - input [ _type "email"; _name (nameof m.Email); _id "email"; _required; _value m.Email ] + input [ _type "email"; _name (nameof model.Email); _id "email"; _required; _value model.Email ] ] ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [ _for (nameof m.Format) ] [ locStr s["E-mail Format"] ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.Format) ] [ locStr s["E-mail Format"] ] types |> Seq.map (fun typ -> fst typ, (snd typ).Value) - |> selectList (nameof m.Format) m.Format [] + |> selectList (nameof model.Format) model.Format [] ] ] - div [ _class "pt-field-row" ] [ submit [] "save" s["Save"] ] + div [ _fieldRow ] [ submit [] "save" s["Save"] ] ] |> List.singleton |> Layout.Content.standard @@ -132,49 +132,49 @@ let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi /// View for the small group log on page -let logOn (groups : SmallGroup list) grpId ctx vi = - let s = I18N.localizer.Force () - let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None } - [ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns"; Target.body ] [ - csrfToken ctx - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] - seq { - if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value - else - "", selectDefault s["Select Group"].Value - yield! - groups - |> List.map (fun grp -> flatGuid grp.smallGroupId, $"{grp.church.name} | {grp.name}") - } - |> 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 ] - ] +let logOn (groups : SmallGroup list) grpId ctx viewInfo = + let s = I18N.localizer.Force () + let model = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None } + let vi = AppViewInfo.withOnLoadScript "PT.smallGroup.logOn.onPageLoad" viewInfo + form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns"; Target.body ] [ + csrfToken ctx + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.SmallGroupId) ] [ locStr s["Group"] ] + seq { + if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value + else + "", selectDefault s["Select Group"].Value + yield! + groups + |> List.map (fun grp -> flatGuid grp.smallGroupId, $"{grp.church.name} | {grp.name}") + } + |> selectList (nameof model.SmallGroupId) grpId [ _required ] ] - div [ _class "pt-checkbox-field" ] [ - 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 [ _inputField ] [ + label [ _for "password" ] [ locStr s["Password"] ] + input [ _type "password" + _name (nameof model.Password) + _id "password" + _placeholder (s["Case-Sensitive"].Value.ToLower ()) + _required ] ] - div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ] ] - script [] [ rawText "PT.onLoad(PT.smallGroup.logOn.onPageLoad)" ] + div [ _checkboxField ] [ + input [ _type "checkbox"; _name (nameof model.RememberMe); _id "rememberMe"; _value "True" ] + label [ _for "rememberMe" ] [ locStr s["Remember Me"] ] + br [] + small [] [ em [] [ str (s["Requires Cookies"].Value.ToLower ()) ] ] + ] + div [ _fieldRow ] [ submit [] "account_circle" s["Log On"] ] ] + |> List.singleton |> Layout.Content.standard |> Layout.standard vi "Group Log On" /// View for the small group maintenance page -let maintain (groups : SmallGroup list) ctx vi = +let maintain (groups : SmallGroup list) ctx viewInfo = let s = I18N.localizer.Force () let grpTbl = match groups with @@ -223,11 +223,11 @@ let maintain (groups : SmallGroup list) ctx vi = form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] |> Layout.Content.standard - |> Layout.standard vi "Maintain Groups" + |> Layout.standard viewInfo "Maintain Groups" /// View for the member maintenance page -let members (members : Member list) (emailTyps : Map) ctx vi = +let members (members : Member list) (emailTypes : Map) ctx viewInfo = let s = I18N.localizer.Force () let mbrTbl = match members with @@ -262,7 +262,7 @@ let members (members : Member list) (emailTyps : Map) c ] td [] [ str mbr.memberName ] td [] [ str mbr.email ] - td [] [ locStr emailTyps[defaultArg mbr.format ""] ] + td [] [ locStr emailTypes[defaultArg mbr.format ""] ] ]) |> tbody [] ] @@ -279,13 +279,13 @@ let members (members : Member list) (emailTyps : Map) c form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] |> Layout.Content.standard - |> Layout.standard vi "Maintain Group Members" + |> Layout.standard viewInfo "Maintain Group Members" open Giraffe.ViewEngine.Accessibility /// View for the small group overview page -let overview m vi = +let overview model viewInfo = let s = I18N.localizer.Force () let linkSpacer = rawText "  " let types = ReferenceList.requestTypeList s |> dict @@ -304,16 +304,16 @@ let overview m vi = header [ _roleHeading ] [ iconSized 72 "question_answer"; locStr s["Prayer Requests"] ] div [] [ p [ _class "pt-center-text" ] [ - strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ] + strong [] [ str (model.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ] ] hr [] - for cat in m.ActiveReqsByType do + for cat in model.ActiveReqsByType do str (cat.Value.ToString "N0") space locStr types[cat.Key] br [] br [] - str (m.AllReqs.ToString "N0") + str (model.AllReqs.ToString "N0") space locStr s["Total Requests"] hr [] @@ -325,7 +325,7 @@ let overview m vi = section [ _ariaLabel "Small group members" ] [ header [ _roleHeading ] [ iconSized 72 "people_outline"; locStr s["Group Members"] ] div [ _class "pt-center-text" ] [ - strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ] + strong [] [ str (model.TotalMembers.ToString "N0"); space; locStr s["Members"] ] hr [] a [ _href "/small-group/members" ] [ icon "email"; linkSpacer; locStr s["Maintain Group Members"] ] ] @@ -333,236 +333,244 @@ let overview m vi = ] |> List.singleton |> Layout.Content.wide - |> Layout.standard vi "Small Group Overview" + |> Layout.standard viewInfo "Small Group Overview" open System.IO open PrayerTracker /// View for the small group preferences page -let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = +let preferences (model : EditPreferences) (tzs : TimeZone list) ctx viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "SmallGroup/Preferences" use sw = new StringWriter () let raw = rawLocText sw - [ 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%; } }" + let vi = + viewInfo + |> AppViewInfo.withScopedStyles [ + "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; }" + "#emailFromAddress { width: 20rem; }" + "#fonts { width: 40rem; }" + "@media screen and (max-width: 40rem) { #fonts { width: 100%; } }" ] - form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ - csrfToken ctx - fieldset [] [ - legend [] [ strong [] [ icon "date_range"; rawText "  "; locStr s["Dates"] ] ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ] - span [] [ - input [ _type "number" - _name (nameof m.ExpireDays) - _id "expireDays" - _value (string m.ExpireDays) - _min "1"; _max "30" - _required - _autofocus ] - space - str (s["Days"].Value.ToLower ()) - ] - ] - div [ _class "pt-field" ] [ - label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ] - span [] [ - input [ _type "number" - _name (nameof m.DaysToKeepNew) - _id "daysToKeepNew" - _min "1"; _max "30" - _value (string m.DaysToKeepNew) - _required ] - space; str (s["Days"].Value.ToLower ()) - ] - ] - div [ _class "pt-field" ] [ - label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ] - span [] [ - input [ _type "number" - _name (nameof m.LongTermUpdateWeeks) - _id "longTermUpdateWeeks" - _min "1"; _max "30" - _value (string m.LongTermUpdateWeeks) - _required ] - space; str (s["Weeks"].Value.ToLower ()) - ] - ] - ] - ] - fieldset [] [ - legend [] [ strong [] [ icon "sort"; rawText "  "; locStr s["Request Sorting"] ] ] - radio (nameof m.RequestSort) "requestSort_D" "D" m.RequestSort - label [ _for "requestSort_D" ] [ locStr s["Sort by Last Updated Date"] ] - rawText "   " - radio (nameof m.RequestSort) "requestSort_R" "R" m.RequestSort - label [ _for "requestSort_R" ] [ locStr s["Sort by Requestor Name"] ] - ] - fieldset [] [ - legend [] [ strong [] [ icon "mail_outline"; rawText "  "; 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" - _value m.EmailFromName - _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" ] [ - label [ _for (nameof m.DefaultEmailType) ] [ locStr s["E-mail Format"] ] - seq { - "", selectDefault s["Select"].Value - yield! - ReferenceList.emailTypeList HtmlFormat s - |> Seq.skip 1 - |> Seq.map (fun typ -> fst typ, (snd typ).Value) - } - |> selectList (nameof m.DefaultEmailType) m.DefaultEmailType [ _required ] - ] - ] - ] - fieldset [] [ - legend [] [ strong [] [ icon "color_lens"; rawText "  "; 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 "    "; 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 "    "; 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 "  "; locStr s["Fonts"] ] ] - div [ _class "pt-field" ] [ - 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 "  "; 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"] ] + |> AppViewInfo.withOnLoadScript "PT.smallGroup.preferences.onPageLoad" + form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ + csrfToken ctx + fieldset [] [ + legend [] [ strong [] [ icon "date_range"; rawText "  "; locStr s["Dates"] ] ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ] span [] [ - radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``) - (string m.Visibility) - label [ _for "viz_Public" ] [ locStr s["Public"] ] - rawText "  " - radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``) - (string m.Visibility) - label [ _for "viz_Private" ] [ locStr s["Private"] ] - rawText "  " - 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" - _value (string m.PageSize) - _required ] + _name (nameof model.ExpireDays) + _id "expireDays" + _value (string model.ExpireDays) + _min "1"; _max "30" + _required + _autofocus ] + space + str (s["Days"].Value.ToLower ()) ] - 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 [ _inputField ] [ + label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ] + span [] [ + input [ _type "number" + _name (nameof model.DaysToKeepNew) + _id "daysToKeepNew" + _min "1"; _max "30" + _value (string model.DaysToKeepNew) + _required ] + space; str (s["Days"].Value.ToLower ()) + ] + ] + div [ _inputField ] [ + label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ] + span [] [ + input [ _type "number" + _name (nameof model.LongTermUpdateWeeks) + _id "longTermUpdateWeeks" + _min "1"; _max "30" + _value (string model.LongTermUpdateWeeks) + _required ] + space; str (s["Weeks"].Value.ToLower ()) ] ] ] - div [ _class "pt-field-row" ] [ submit [] "save" s["Save Preferences"] ] ] + fieldset [] [ + legend [] [ strong [] [ icon "sort"; rawText "  "; locStr s["Request Sorting"] ] ] + radio (nameof model.RequestSort) "requestSort_D" "D" model.RequestSort + label [ _for "requestSort_D" ] [ locStr s["Sort by Last Updated Date"] ] + rawText "   " + radio (nameof model.RequestSort) "requestSort_R" "R" model.RequestSort + label [ _for "requestSort_R" ] [ locStr s["Sort by Requestor Name"] ] + ] + fieldset [] [ + legend [] [ strong [] [ icon "mail_outline"; rawText "  "; locStr s["E-mail"] ] ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "emailFromName" ] [ locStr s["From Name"] ] + input [ _type "text" + _name (nameof model.EmailFromName) + _id "emailFromName" + _value model.EmailFromName + _required ] + ] + div [ _inputField ] [ + label [ _for "emailFromAddress" ] [ locStr s["From Address"] ] + input [ _type "email" + _name (nameof model.EmailFromAddress) + _id "emailFromAddress" + _value model.EmailFromAddress + _required ] + ] + ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.DefaultEmailType) ] [ locStr s["E-mail Format"] ] + seq { + "", selectDefault s["Select"].Value + yield! + ReferenceList.emailTypeList HtmlFormat s + |> Seq.skip 1 + |> Seq.map (fun typ -> fst typ, (snd typ).Value) + } + |> selectList (nameof model.DefaultEmailType) model.DefaultEmailType [ _required ] + ] + ] + ] + fieldset [] [ + legend [] [ strong [] [ icon "color_lens"; rawText "  "; locStr s["Colors"] ]; rawText " ***" ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _class "pt-center-text" ] [ locStr s["Color of Heading Lines"] ] + span [] [ + radio (nameof model.LineColorType) "lineColorType_Name" "Name" model.LineColorType + label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ] + namedColorList (nameof model.LineColor) model.LineColor [ + _id "lineColor_Select" + if model.LineColor.StartsWith "#" then _disabled ] s + rawText "    "; str (s["or"].Value.ToUpper ()) + radio (nameof model.LineColorType) "lineColorType_RGB" "RGB" model.LineColorType + label [ _for "lineColorType_RGB" ] [ locStr s["Custom Color"] ] + input [ _type "color" + _name (nameof model.LineColor) + _id "lineColor_Color" + _value model.LineColor // TODO: convert to hex or skip if named + if not (model.LineColor.StartsWith "#") then _disabled ] + ] + ] + ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _class "pt-center-text" ] [ locStr s["Color of Heading Text"] ] + span [] [ + radio (nameof model.HeadingColorType) "headingColorType_Name" "Name" model.HeadingColorType + label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ] + namedColorList (nameof model.HeadingColor) model.HeadingColor [ + _id "headingColor_Select" + if model.HeadingColor.StartsWith "#" then _disabled ] s + rawText "    "; str (s["or"].Value.ToUpper ()) + radio (nameof model.HeadingColorType) "headingColorType_RGB" "RGB" model.HeadingColorType + label [ _for "headingColorType_RGB" ] [ locStr s["Custom Color"] ] + input [ _type "color" + _name (nameof model.HeadingColor) + _id "headingColor_Color" + _value model.HeadingColor // TODO: convert to hex or skip if named + if not (model.HeadingColor.StartsWith "#") then _disabled ] + ] + ] + ] + ] + fieldset [] [ + legend [] [ strong [] [ icon "font_download"; rawText "  "; locStr s["Fonts"] ] ] + div [ _inputField ] [ + label [ _for "fonts" ] [ locStr s["Fonts** for List"] ] + input [ _type "text"; _name (nameof model.Fonts); _id "fonts"; _required; _value model.Fonts ] + ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "headingFontSize" ] [ locStr s["Heading Text Size"] ] + input [ _type "number" + _name (nameof model.HeadingFontSize) + _id "headingFontSize" + _min "8"; _max "24" + _value (string model.HeadingFontSize) + _required ] + ] + div [ _inputField ] [ + label [ _for "listFontSize" ] [ locStr s["List Text Size"] ] + input [ _type "number" + _name (nameof model.ListFontSize) + _id "listFontSize" + _min "8"; _max "24" + _value (string model.ListFontSize) + _required ] + ] + ] + ] + fieldset [] [ + legend [] [ strong [] [ icon "settings"; rawText "  "; locStr s["Other Settings"] ] ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.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 model.TimeZone) model.TimeZone [ _required ] + ] + ] + div [ _inputField ] [ + label [] [ locStr s["Request List Visibility"] ] + span [] [ + radio (nameof model.Visibility) "viz_Public" (string RequestVisibility.``public``) + (string model.Visibility) + label [ _for "viz_Public" ] [ locStr s["Public"] ] + rawText "  " + radio (nameof model.Visibility) "viz_Private" (string RequestVisibility.``private``) + (string model.Visibility) + label [ _for "viz_Private" ] [ locStr s["Private"] ] + rawText "  " + radio (nameof model.Visibility) "viz_Password" (string RequestVisibility.passwordProtected) + (string model.Visibility) + label [ _for "viz_Password" ] [ locStr s["Password Protected"] ] + ] + ] + let classSuffix = if model.Visibility = RequestVisibility.passwordProtected then [ "pt-show" ] else [] + div [ _id "divClassPassword"; _fieldRowWith ("pt-fadeable" :: classSuffix) ] [ + div [ _inputField ] [ + label [ _for "groupPassword" ] [ locStr s["Group Password (Used to Read Online)"] ] + input [ _type "text" + _name (nameof model.GroupPassword) + _id "groupPassword" + _value (defaultArg model.GroupPassword "") ] + ] + ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "pageSize" ] [ locStr s["Page Size"] ] + input [ _type "number" + _name (nameof model.PageSize) + _id "pageSize" + _min "10"; _max "255" + _value (string model.PageSize) + _required ] + ] + div [ _inputField ] [ + label [ _for (nameof model.AsOfDate) ] [ locStr s["“As of” Date Display"] ] + ReferenceList.asOfDateList s + |> List.map (fun (code, desc) -> code, desc.Value) + |> selectList (nameof model.AsOfDate) model.AsOfDate [ _required ] + ] + ] + ] + div [ _fieldRow ] [ submit [] "save" s["Save Preferences"] ] + ] + |> List.singleton + |> List.append [ p [] [ rawText "** " raw l["List font names, separated by commas."] @@ -575,7 +583,6 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = 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 HTML color name list."] ] - script [] [ rawText "PT.onLoad(PT.smallGroup.preferences.onPageLoad)" ] ] |> Layout.Content.standard |> Layout.standard vi "Group Preferences" diff --git a/src/PrayerTracker.UI/User.fs b/src/PrayerTracker.UI/User.fs index c9d2b50..708fcf7 100644 --- a/src/PrayerTracker.UI/User.fs +++ b/src/PrayerTracker.UI/User.fs @@ -4,13 +4,13 @@ open Giraffe.ViewEngine open PrayerTracker.ViewModels /// View for the group assignment page -let assignGroups m groups curGroups ctx vi = +let assignGroups model groups curGroups ctx viewInfo = let s = I18N.localizer.Force () - let pageTitle = sprintf "%s • %A" m.UserName s["Assign Groups"] + let pageTitle = sprintf "%s • %A" model.UserName s["Assign Groups"] form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns"; Target.content ] [ csrfToken ctx - 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 model.UserId); _value (flatGuid model.UserId) ] + input [ _type "hidden"; _name (nameof model.UserName); _value model.UserName ] table [ _class "pt-table" ] [ thead [] [ tr [] [ @@ -24,7 +24,7 @@ let assignGroups m groups curGroups ctx vi = tr [] [ td [] [ input [ _type "checkbox" - _name (nameof m.SmallGroups) + _name (nameof model.SmallGroups) _id inputId _value grpId if List.contains grpId curGroups then _checked ] @@ -33,43 +33,53 @@ let assignGroups m groups curGroups ctx vi = ]) |> tbody [] ] - div [ _class "pt-field-row" ] [ submit [] "save" s["Save Group Assignments"] ] + div [ _fieldRow ] [ submit [] "save" s["Save Group Assignments"] ] ] |> List.singleton |> Layout.Content.standard - |> Layout.standard vi pageTitle + |> Layout.standard viewInfo pageTitle /// View for the password change page -let changePassword ctx vi = +let changePassword ctx viewInfo = let s = I18N.localizer.Force () - let m = { OldPassword = ""; NewPassword = ""; NewPasswordConfirm = "" } - [ 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."] - ] - style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "] + let model = { OldPassword = ""; NewPassword = ""; NewPasswordConfirm = "" } + let vi = + AppViewInfo.withScopedStyles [ "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; }"] viewInfo + 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."] + ] + |> List.singleton + |> List.append [ form [ _action "/user/password/change" _method "post" _onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')""" Target.content ] [ csrfToken ctx - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ + div [ _fieldRow ] [ + div [ _inputField ] [ label [ _for "oldPassword" ] [ locStr s["Current Password"] ] - input [ _type "password"; _name (nameof m.OldPassword); _id "oldPassword"; _required; _autofocus ] + input [ _type "password" + _name (nameof model.OldPassword) + _id "oldPassword" + _required + _autofocus ] ] ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ + div [ _fieldRow ] [ + div [ _inputField ] [ label [ _for "newPassword" ] [ locStr s["New Password Twice"] ] - input [ _type "password"; _name (nameof m.NewPassword); _id "newPassword"; _required ] + input [ _type "password"; _name (nameof model.NewPassword); _id "newPassword"; _required ] ] - div [ _class "pt-field" ] [ + div [ _inputField ] [ label [] [ rawText " " ] - input [ _type "password"; _name (nameof m.NewPasswordConfirm); _id "newPasswordConfirm"; _required ] + input [ _type "password" + _name (nameof model.NewPasswordConfirm) + _id "newPasswordConfirm" + _required ] ] ] - div [ _class "pt-field-row" ] [ + div [ _fieldRow ] [ submit [ _onclick "document.getElementById('newPasswordConfirm').setCustomValidity('')" ] "done" s["Change Your Password"] ] @@ -80,102 +90,110 @@ let changePassword ctx vi = /// View for the edit user page -let edit (m : EditUser) ctx vi = +let edit (model : EditUser) ctx viewInfo = let s = I18N.localizer.Force () - let pageTitle = if m.IsNew then "Add a New User" else "Edit User" - let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value - [ style [ _scoped ] - [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ] - form [ _action "/user/edit/save" - _method "post" - _class "pt-center-columns" - _onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" - Target.content ] [ - csrfToken ctx - input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [ _for "firstName" ] [ locStr s["First Name"] ] - 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-row" ] [ - 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"] ] + let pageTitle = if model.IsNew then "Add a New User" else "Edit User" + let pwPlaceholder = s[if model.IsNew then "" else "No change"].Value + let vi = + viewInfo + |> AppViewInfo.withScopedStyles [ + "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; }" + "#email { width: 20rem; }" ] - script [] [ rawText $"PT.onLoad(PT.user.edit.onPageLoad({(string m.IsNew).ToLowerInvariant ()}))" ] + |> AppViewInfo.withOnLoadScript $"PT.user.edit.onPageLoad({(string model.IsNew).ToLowerInvariant ()})" + form [ _action "/user/edit/save" + _method "post" + _class "pt-center-columns" + _onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" + Target.content ] [ + csrfToken ctx + input [ _type "hidden"; _name (nameof model.UserId); _value (flatGuid model.UserId) ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "firstName" ] [ locStr s["First Name"] ] + input [ _type "text" + _name (nameof model.FirstName) + _id "firstName" + _value model.FirstName + _required + _autofocus ] + ] + div [ _inputField ] [ + label [ _for "lastName" ] [ locStr s["Last Name"] ] + input [ _type "text"; _name (nameof model.LastName); _id "lastName"; _value model.LastName; _required ] + ] + div [ _inputField ] [ + label [ _for "email" ] [ locStr s["E-mail Address"] ] + input [ _type "email"; _name (nameof model.Email); _id "email"; _value model.Email; _required ] + ] + ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for "password" ] [ locStr s["Password"] ] + input [ _type "password"; _name (nameof model.Password); _id "password"; _placeholder pwPlaceholder ] + ] + div [ _inputField ] [ + label [ _for "passwordConfirm" ] [ locStr s["Password Again"] ] + input [ _type "password" + _name (nameof model.PasswordConfirm) + _id "passwordConfirm" + _placeholder pwPlaceholder ] + ] + ] + div [ _checkboxField ] [ + input [ _type "checkbox" + _name (nameof model.IsAdmin) + _id "isAdmin" + _value "True" + if defaultArg model.IsAdmin false then _checked ] + label [ _for "isAdmin" ] [ locStr s["This user is a PrayerTracker administrator"] ] + ] + div [ _fieldRow ] [ submit [] "save" s["Save User"] ] ] + |> List.singleton |> Layout.Content.standard |> Layout.standard vi pageTitle - /// View for the user log on page -let logOn (m : UserLogOn) groups ctx vi = - let s = I18N.localizer.Force () +let logOn (model : UserLogOn) groups ctx viewInfo = + let s = I18N.localizer.Force () + let vi = AppViewInfo.withScopedStyles [ "#email { width: 20rem; }" ] viewInfo form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns"; Target.body ] [ - style [ _scoped ] [ rawText "#email { width: 20rem; }" ] csrfToken ctx - input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ + input [ _type "hidden"; _name (nameof model.RedirectUrl); _value (defaultArg model.RedirectUrl "") ] + div [ _fieldRow ] [ + div [ _inputField ] [ label [ _for "email"] [ locStr s["E-mail Address"] ] - input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required; _autofocus ] + input [ _type "email" + _name (nameof model.Email) + _id "email" + _value model.Email + _required + _autofocus ] ] - div [ _class "pt-field" ] [ + div [ _inputField ] [ label [ _for "password" ] [ locStr s["Password"] ] input [ _type "password" - _name (nameof m.Password) + _name (nameof model.Password) _id "password" _placeholder $"""({s["Case-Sensitive"].Value.ToLower ()})""" _required ] ] ] - div [ _class "pt-field-row" ] [ - div [ _class "pt-field" ] [ - label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] + div [ _fieldRow ] [ + div [ _inputField ] [ + label [ _for (nameof model.SmallGroupId) ] [ locStr s["Group"] ] seq { "", selectDefault s["Select Group"].Value; yield! groups } - |> selectList (nameof m.SmallGroupId) "" [ _required ] + |> selectList (nameof model.SmallGroupId) "" [ _required ] ] ] - div [ _class "pt-checkbox-field" ] [ - input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ] + div [ _checkboxField ] [ + input [ _type "checkbox"; _name (nameof model.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"] ] + div [ _fieldRow ] [ submit [] "account_circle" s["Log On"] ] ] |> List.singleton |> Layout.Content.standard @@ -185,7 +203,7 @@ let logOn (m : UserLogOn) groups ctx vi = open PrayerTracker.Entities /// View for the user maintenance page -let maintain (users : User list) ctx vi = +let maintain (users : User list) ctx viewInfo = let s = I18N.localizer.Force () let usrTbl = match users with @@ -237,4 +255,4 @@ let maintain (users : User list) ctx vi = form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] |> Layout.Content.standard - |> Layout.standard vi "Maintain Users" + |> Layout.standard viewInfo "Maintain Users" diff --git a/src/PrayerTracker.UI/ViewModels.fs b/src/PrayerTracker.UI/ViewModels.fs index 5f2f575..38a11c8 100644 --- a/src/PrayerTracker.UI/ViewModels.fs +++ b/src/PrayerTracker.UI/ViewModels.fs @@ -147,23 +147,40 @@ type AppViewInfo = /// The layout with which the content will be rendered Layout : LayoutType + + /// Scoped styles for this view + ScopedStyle : string list + + /// A JavaScript function to run on page load + OnLoadScript : string option } + // TODO: add onload script option to this, modify layout to add it /// Support for the AppViewInfo type module AppViewInfo = /// A fresh version that can be populated to process the current request let fresh = - { Style = [] - Script = [] - HelpLink = None - Messages = [] - Version = "" - RequestStart = DateTime.Now.Ticks - User = None - Group = None - Layout = FullPage + { Style = [] + Script = [] + HelpLink = None + Messages = [] + Version = "" + RequestStart = DateTime.Now.Ticks + User = None + Group = None + Layout = FullPage + ScopedStyle = [] + OnLoadScript = None } + + /// Add scoped styles to the given view info object + let withScopedStyles styles viewInfo = + { viewInfo with ScopedStyle = styles } + + /// Add an onload action to the given view info object + let withOnLoadScript script viewInfo = + { viewInfo with OnLoadScript = Some script } /// Form for sending a small group or system-wide announcement diff --git a/src/PrayerTracker/wwwroot/js/app.js b/src/PrayerTracker/wwwroot/js/app.js index e0831b7..27ae2d3 100644 --- a/src/PrayerTracker/wwwroot/js/app.js +++ b/src/PrayerTracker/wwwroot/js/app.js @@ -2,7 +2,7 @@ * This file contains a library of common functions used throughout the PrayerTracker website, as well as specific * functions used on pages throughout the site. */ -const PT = { +this.PT = { /** * Open a window with help