From 5240b784872a316dbac086ddb3d55a2a088cd843 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 30 Jan 2025 15:51:52 -0500 Subject: [PATCH] WIP on fixi implementation --- src/PrayerTracker.Data/DistributedCache.fs | 1 - src/PrayerTracker.UI/CommonFunctions.fs | 67 ++++++----- src/PrayerTracker.UI/I18N.fs | 8 +- src/PrayerTracker.UI/Layout.fs | 105 +++++++++--------- src/PrayerTracker.UI/PrayerTracker.UI.fsproj | 1 + src/PrayerTracker.UI/Utils.fs | 16 +-- src/PrayerTracker.UI/ViewModels.fs | 56 +++++----- src/PrayerTracker/App.fs | 12 +- src/PrayerTracker/Extensions.fs | 19 ++-- src/PrayerTracker/wwwroot/{css => _}/app.css | 0 src/PrayerTracker/wwwroot/{js => _}/app.js | 0 src/PrayerTracker/wwwroot/_/fixi-0.5.7.js | 87 +++++++++++++++ src/PrayerTracker/wwwroot/{css => _}/help.css | 0 13 files changed, 238 insertions(+), 134 deletions(-) rename src/PrayerTracker/wwwroot/{css => _}/app.css (100%) rename src/PrayerTracker/wwwroot/{js => _}/app.js (100%) create mode 100644 src/PrayerTracker/wwwroot/_/fixi-0.5.7.js rename src/PrayerTracker/wwwroot/{css => _}/help.css (100%) diff --git a/src/PrayerTracker.Data/DistributedCache.fs b/src/PrayerTracker.Data/DistributedCache.fs index c3684fe..5bcc84d 100644 --- a/src/PrayerTracker.Data/DistributedCache.fs +++ b/src/PrayerTracker.Data/DistributedCache.fs @@ -5,7 +5,6 @@ open System.Threading.Tasks open Microsoft.Extensions.Caching.Distributed open NodaTime open Npgsql -open Npgsql.FSharp /// Helper types and functions for the cache [] diff --git a/src/PrayerTracker.UI/CommonFunctions.fs b/src/PrayerTracker.UI/CommonFunctions.fs index dd192bc..1c85fbc 100644 --- a/src/PrayerTracker.UI/CommonFunctions.fs +++ b/src/PrayerTracker.UI/CommonFunctions.fs @@ -3,40 +3,45 @@ module PrayerTracker.Views.CommonFunctions open System.IO open System.Text.Encodings.Web -open Giraffe open Giraffe.ViewEngine -open Microsoft.AspNetCore.Antiforgery -open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Mvc.Localization open Microsoft.Extensions.Localization /// Encoded text for a localized string -let locStr (text : LocalizedString) = str text.Value +let locStr (text: LocalizedString) = + str text.Value /// Raw text for a localized HTML string -let rawLocText (writer : StringWriter) (text : LocalizedHtmlString) = - text.WriteTo (writer, HtmlEncoder.Default) +let rawLocText (writer: StringWriter) (text: LocalizedHtmlString) = + text.WriteTo(writer, HtmlEncoder.Default) let txt = string writer - writer.GetStringBuilder().Clear () |> ignore + writer.GetStringBuilder().Clear() |> ignore rawText txt /// A space (used for back-to-back localization string breaks) let space = rawText " " /// Generate a Material Design icon -let icon name = i [ _class "material-icons" ] [ rawText name ] +let icon name = + i [ _class "material-icons" ] [ rawText name ] /// Generate a Material Design icon, specifying the point size (must be defined in CSS) -let iconSized size name = i [ _class $"material-icons md-%i{size}" ] [ rawText name ] +let iconSized size name = + i [ _class $"material-icons md-%i{size}" ] [ rawText name ] + + +open Giraffe +open Microsoft.AspNetCore.Antiforgery +open Microsoft.AspNetCore.Http /// Generate a CSRF prevention token -let csrfToken (ctx : HttpContext) = - let antiForgery = ctx.GetService () +let csrfToken (ctx: HttpContext) = + let antiForgery = ctx.GetService() let tokenSet = antiForgery.GetAndStoreTokens ctx input [ _type "hidden"; _name tokenSet.FormFieldName; _value tokenSet.RequestToken ] /// Create a summary for a table of items -let tableSummary itemCount (s : IStringLocalizer) = +let tableSummary itemCount (s: IStringLocalizer) = div [ _class "pt-center-text" ] [ small [] [ match itemCount with @@ -48,7 +53,7 @@ let tableSummary itemCount (s : IStringLocalizer) = ] /// Generate a list of named HTML colors -let namedColorList name selected attrs (s : IStringLocalizer) = +let namedColorList name selected attrs (s: IStringLocalizer) = // The list of HTML named colors (name, display, text color) seq { ("aqua", s["Aqua"], "black") @@ -79,7 +84,7 @@ let namedColorList name selected attrs (s : IStringLocalizer) = |> select (_name name :: attrs) /// Convert a named color to its hex notation -let colorToHex (color : string) = +let colorToHex (color: string) = match color with | it when it.StartsWith "#" -> color | "aqua" -> "#00ffff" @@ -100,7 +105,7 @@ let colorToHex (color : string) = | "yellow" -> "#ffff00" | it -> it -/// Generate an input[type=radio] that is selected if its value is the current value +/// Generate an input type=radio that is selected if its value is the current value let radio name domId value current = input [ _type "radio" _name name @@ -108,7 +113,7 @@ let radio name domId value current = _value value if value = current then _checked ] -/// Generate a select list with the current value selected +/// Generate a select list with the current value selected let selectList name selected attrs items = items |> Seq.map (fun (value, text) -> @@ -119,16 +124,18 @@ let selectList name selected attrs items = |> List.ofSeq |> select (List.concat [ [ _name name; _id name ]; attrs ]) -/// Generate the text for a default entry at the top of a select list -let selectDefault text = $"— %s{text} —" +/// Generate the text for a default entry at the top of a select list +let selectDefault text = + $"— %s{text} —" -/// Generate a standard submit button with icon and text -let submit attrs ico text = button (_type "submit" :: attrs) [ icon ico; rawText "  "; locStr text ] +/// Generate a standard button type=submit with icon and text +let submit attrs ico text = + button (_type "submit" :: attrs) [ icon ico; rawText "  "; locStr text ] /// Create an HTML onsubmit event handler let _onsubmit = attr "onsubmit" -/// A "rel='noopener'" attribute +/// A rel="noopener" attribute let _relNoOpener = _rel "noopener" /// A class attribute that designates a row of fields, with the additional classes passed @@ -153,12 +160,14 @@ let _checkboxField = _class "pt-checkbox-field" /// A group of related fields, inputs, links, etc., displayed in a row let _group = _class "pt-group" -/// Create an input field of the given type, with matching name and ID and the given value +/// +/// Create an input field of the given type, with matching name and ID and the given value +/// let inputField typ name value attrs = List.concat [ [ _type typ; _name name; _id name; if value <> "" then _value value ]; attrs ] |> input /// Generate a table heading with the given localized column names -let tableHeadings (s : IStringLocalizer) (headings : string list) = +let tableHeadings (s: IStringLocalizer) (headings: string list) = headings |> List.map (fun heading -> th [ _scope "col" ] [ locStr s[heading] ]) |> tr [] @@ -166,12 +175,20 @@ let tableHeadings (s : IStringLocalizer) (headings : string list) = |> thead [] /// For a list of strings, prepend a pound sign and string them together with commas (CSS selector by ID) -let toHtmlIds it = it |> List.map (fun x -> $"#%s{x}") |> String.concat ", " +let toHtmlIds it = + it |> List.map (fun x -> $"#%s{x}") |> String.concat ", " /// The name this function used to have when the view engine was part of Giraffe let renderHtmlNode = RenderView.AsString.htmlNode +open Giraffe.Fixi + +/// Create a page link that will make the request with fixi +let pageLink href attrs content = + a (List.append [ _href href; _fxGet; _fxAction href; _fxTarget "#pt-body" ] attrs) content + + open Microsoft.AspNetCore.Html /// Render an HTML node, then return the value as an HTML string @@ -194,7 +211,7 @@ module TimeZones = ] /// Get the name of a time zone, given its Id - let name timeZoneId (s : IStringLocalizer) = + let name timeZoneId (s: IStringLocalizer) = match xref |> List.tryFind (fun it -> fst it = timeZoneId) with | Some tz -> s[snd tz] | None -> diff --git a/src/PrayerTracker.UI/I18N.fs b/src/PrayerTracker.UI/I18N.fs index 31cb66c..9b1d6a6 100644 --- a/src/PrayerTracker.UI/I18N.fs +++ b/src/PrayerTracker.UI/I18N.fs @@ -12,11 +12,11 @@ let private resAsmName = typeof.Assembly.GetName().Name /// Set up the string and HTML localizer factories let setUpFactories fac = stringLocFactory <- fac - htmlLocFactory <- HtmlLocalizerFactory stringLocFactory + htmlLocFactory <- HtmlLocalizerFactory stringLocFactory /// An instance of the common string localizer -let localizer = lazy (stringLocFactory.Create ("Common", resAsmName)) +let localizer = lazy stringLocFactory.Create("Common", resAsmName) /// Get a view localizer -let forView (view : string) = - htmlLocFactory.Create ($"Views.{view.Replace ('/', '.')}", resAsmName) +let forView (view: string) = + htmlLocFactory.Create($"Views.{view.Replace('/', '.')}", resAsmName) diff --git a/src/PrayerTracker.UI/Layout.fs b/src/PrayerTracker.UI/Layout.fs index 583d7d5..2e5e64e 100644 --- a/src/PrayerTracker.UI/Layout.fs +++ b/src/PrayerTracker.UI/Layout.fs @@ -25,20 +25,25 @@ module Navigation = a [ _dropdown; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [ icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down" ] div [ _class "dropdown-content"; _roleMenuBar ] [ - a [ _href "/prayer-requests"; _roleMenuItem ] [ - icon "compare_arrows"; menuSpacer; locStr s["Maintain"] ] - a [ _href "/prayer-requests/view"; _roleMenuItem ] [ - icon "list"; menuSpacer; locStr s["View List"] ] ] ] + pageLink "/prayer-requests" + [ _roleMenuItem ] + [ icon "compare_arrows"; menuSpacer; locStr s["Maintain"] ] + pageLink "/prayer-requests/view" + [ _roleMenuItem ] + [ icon "list"; menuSpacer; locStr s["View List"] ] ] ] li [ _class "dropdown" ] [ a [ _dropdown; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [ icon "group"; space; locStr s["Group"]; space; icon "keyboard_arrow_down" ] div [ _class "dropdown-content"; _roleMenuBar ] [ - a [ _href "/small-group/members"; _roleMenuItem ] [ - icon "email"; menuSpacer; locStr s["Maintain Group Members"] ] - a [ _href "/small-group/announcement"; _roleMenuItem ] [ - icon "send"; menuSpacer; locStr s["Send Announcement"] ] - a [ _href "/small-group/preferences"; _roleMenuItem ] [ - icon "build"; menuSpacer; locStr s["Change Preferences"] ] ] ] + pageLink "/small-group/members" + [ _roleMenuItem ] + [ icon "email"; menuSpacer; locStr s["Maintain Group Members"] ] + pageLink "/small-group/announcement" + [ _roleMenuItem ] + [ icon "send"; menuSpacer; locStr s["Send Announcement"] ] + pageLink "/small-group/preferences" + [ _roleMenuItem ] + [ icon "build"; menuSpacer; locStr s["Change Preferences"] ] ] ] if u.IsAdmin then li [ _class "dropdown" ] [ a [ _dropdown @@ -47,31 +52,31 @@ module Navigation = _roleButton ] [ icon "settings"; space; locStr s["Administration"]; space; icon "keyboard_arrow_down" ] div [ _class "dropdown-content"; _roleMenuBar ] [ - a [ _href "/churches"; _roleMenuItem ] [ icon "home"; menuSpacer; locStr s["Churches"] ] - a [ _href "/small-groups"; _roleMenuItem ] [ - icon "send"; menuSpacer; locStr s["Groups"] ] - a [ _href "/users"; _roleMenuItem ] [ icon "build"; menuSpacer; locStr s["Users"] ] ] ] + pageLink "/churches" [ _roleMenuItem ] [ icon "home"; menuSpacer; locStr s["Churches"] ] + pageLink "/small-groups" + [ _roleMenuItem ] + [ icon "send"; menuSpacer; locStr s["Groups"] ] + pageLink "/users" [ _roleMenuItem ] [ icon "build"; menuSpacer; locStr s["Users"] ] ] ] | None -> match m.Group with | Some _ -> li [] [ - a [ _href "/prayer-requests/view" - _ariaLabel s["View Request List"].Value - _title s["View Request List"].Value ] [ - icon "list"; space; locStr s["View Request List"] ] ] + pageLink "/prayer-requests/view" + [ _ariaLabel s["View Request List"].Value; _title s["View Request List"].Value ] + [ icon "list"; space; locStr s["View Request List"] ] ] | None -> li [ _class "dropdown" ] [ 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" ] div [ _class "dropdown-content"; _roleMenuBar ] [ - a [ _href "/user/log-on"; _roleMenuItem ] [ icon "person"; menuSpacer; locStr s["User"] ] - a [ _href "/small-group/log-on"; _roleMenuItem ] [ - icon "group"; menuSpacer; locStr s["Group"] ] ] ] + pageLink "/user/log-on" [ _roleMenuItem ] [ icon "person"; menuSpacer; locStr s["User"] ] + pageLink "/small-group/log-on" + [ _roleMenuItem ] + [ icon "group"; menuSpacer; locStr s["Group"] ] ] ] li [] [ - a [ _href "/prayer-requests/lists" - _ariaLabel s["View Request List"].Value - _title s["View Request List"].Value ] [ - icon "list"; space; locStr s["View Request List"] ] ] + pageLink "/prayer-requests/lists" + [ _ariaLabel s["View Request List"].Value; _title s["View Request List"].Value ] + [ icon "list"; space; locStr s["View Request List"] ] ] li [] [ a [ _href "/help"; _ariaLabel s["Help"].Value; _title s["View Help"].Value; _target "_blank" ] [ icon "help"; space; locStr s["Help"] ] ] ] @@ -81,19 +86,19 @@ module Navigation = match m.User with | Some _ -> li [] [ - a [ _href "/user/password" - _ariaLabel s["Change Your Password"].Value - _title s["Change Your Password"].Value ] [ - icon "lock"; space; locStr s["Change Your Password"] ] ] + pageLink "/user/password" + [ _ariaLabel s["Change Your Password"].Value; _title s["Change Your Password"].Value ] + [ icon "lock"; space; locStr s["Change Your Password"] ] ] | None -> () li [] [ - a [ _href "/log-off"; _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ] [ - icon "power_settings_new"; space; locStr s["Log Off"] ] ] ] + pageLink "/log-off" + [ _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ] + [ icon "power_settings_new"; space; locStr s["Log Off"] ] ] ] | None -> [] header [ _class "pt-title-bar"; Target.content ] [ section [ _class "pt-title-bar-left"; _ariaLabel "Left side of top menu" ] [ span [ _class "pt-title-bar-home" ] [ - a [ _href "/"; _title s["Home"].Value ] [ locStr s["PrayerTracker"] ] ] + pageLink "/" [ _title s["Home"].Value ] [ locStr s["PrayerTracker"] ] ] ul [] leftLinks ] section [ _class "pt-title-bar-center"; _ariaLabel "Empty center space in top menu" ] [] section [ _class "pt-title-bar-right"; _roleToolBar; _ariaLabel "Right side of top menu" ] [ @@ -109,11 +114,11 @@ module Navigation = | "es" -> strong [] [ locStr s["Spanish"] ] rawText "     " - a [ _href "/language/en" ] [ locStr s["Change to English"] ] + pageLink "/language/en" [] [ locStr s["Change to English"] ] | _ -> strong [] [ locStr s["English"] ] rawText "     " - a [ _href "/language/es" ] [ locStr s["Cambie a Español"] ] ] + pageLink "/language/es" [] [ locStr s["Cambie a Español"] ] ] match m.Group with | Some g -> [ match m.User with @@ -129,7 +134,7 @@ module Navigation = icon "group" space match m.User with - | Some _ -> a [ _href "/small-group"; Target.content ] [ strong [] [ str g.Name ] ] + | Some _ -> pageLink "/small-group" [] [ strong [] [ str g.Name ] ] | None -> strong [] [ str g.Name ] ] | None -> [] |> div [] ] @@ -153,7 +158,7 @@ let private commonHead = [ meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] meta [ _name "generator"; _content "Giraffe" ] link [ _rel "stylesheet"; _href "https://fonts.googleapis.com/icon?family=Material+Icons" ] - link [ _rel "stylesheet"; _href "/css/app.css" ] ] + link [ _rel "stylesheet"; _href "/_/app.css" ] ] /// Render the portion of the page let private htmlHead viewInfo pgTitle = @@ -163,19 +168,16 @@ let private htmlHead viewInfo pgTitle = title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ] yield! commonHead for cssFile in viewInfo.Style do - link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ] ] + link [ _rel "stylesheet"; _href $"/_/{cssFile}.css"; _type "text/css" ] ] -open Giraffe.ViewEngine.Htmx - /// Render a link to the help page for the current page let private helpLink link = let s = I18N.localizer.Force() sup [ _class "pt-help-link" ] [ a [ _href link _title s["Click for Help on This Page"].Value - _onclick $"return PT.showHelp('{link}')" - _hxNoBoost ] [ iconSized 18 "help_outline" ] ] + _onclick $"return PT.showHelp('{link}')" ] [ iconSized 18 "help_outline" ] ] /// Render the page title, and optionally a help link let private renderPageTitle viewInfo pgTitle = @@ -217,9 +219,9 @@ let private htmlFooter viewInfo = let resultTime = (SystemClock.Instance.GetCurrentInstant() - viewInfo.RequestStart).TotalSeconds footer [ _class "pt-footer" ] [ div [ _id "pt-legal" ] [ - a [ _href "/legal/privacy-policy" ] [ locStr s["Privacy Policy"] ] + pageLink "/legal/privacy-policy" [] [ locStr s["Privacy Policy"] ] rawText "   " - a [ _href "/legal/terms-of-service" ] [ locStr s["Terms of Service"] ] + pageLink "/legal/terms-of-service" [] [ locStr s["Terms of Service"] ] rawText "   " a [ _href "https://git.bitbadger.solutions/bit-badger/PrayerTracker" _title s["View source code and get technical support"].Value @@ -227,7 +229,7 @@ let private htmlFooter viewInfo = _relNoOpener ] [ locStr s["Source & Support"] ] ] div [ _id "pt-footer" ] [ - a [ _href "/"; _style "line-height:28px;" ] [ + pageLink "/" [ _style "line-height:28px;" ] [ img [ _src $"""/img/%O{s["footer_en"]}.png""" _alt imgText _title imgText @@ -268,19 +270,16 @@ let private partialHead pgTitle = 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 pgTitle content = - body [ _hxBoost ] [ + body [] [ Navigation.top viewInfo - div [ _id "pt-body"; Target.content; _hxSwap $"{HxSwap.InnerHtml} show:window:top" ] - (contentSection viewInfo pgTitle content) + div [ _id "pt-body" ] (contentSection viewInfo pgTitle content) match viewInfo.Layout with | FullPage -> - Script.minified script [ _src "/js/ckeditor/ckeditor.js" ] [] - script [ _src "/js/app.js" ] [] + script [ _src "/_/fixi-0.5.7.js" ] [] + script [ _src "/_/app.js" ] [] | _ -> () ] /// The standard layout(s) for PrayerTracker @@ -316,8 +315,8 @@ let help pageTitle isHome content = meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker Help"] ] link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ] - link [ _href "/css/app.css"; _rel "stylesheet" ] - link [ _href "/css/help.css"; _rel "stylesheet" ] ] + link [ _href "/_/app.css"; _rel "stylesheet" ] + link [ _href "/_/help.css"; _rel "stylesheet" ] ] body [] [ header [ _class "pt-title-bar" ] [ section [ _class "pt-title-bar-left" ] [ diff --git a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj index dc63445..89d2f33 100644 --- a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj +++ b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj @@ -15,6 +15,7 @@ + diff --git a/src/PrayerTracker.UI/Utils.fs b/src/PrayerTracker.UI/Utils.fs index 2c48b2a..30bfafe 100644 --- a/src/PrayerTracker.UI/Utils.fs +++ b/src/PrayerTracker.UI/Utils.fs @@ -19,10 +19,12 @@ let emptyGuid = shortGuid Guid.Empty module String = /// string.Trim() - let trim (str: string) = str.Trim() + let trim (str: string) = + str.Trim() /// string.Replace() - let replace (find: string) repl (str: string) = str.Replace(find, repl) + let replace (find: string) repl (str: string) = + str.Replace(find, repl) /// Replace the first occurrence of a string with a second string within a given string let replaceFirst (needle: string) replacement (haystack: string) = @@ -51,7 +53,7 @@ let stripTags allowedTags input = allowedTags |> List.fold (fun acc t -> acc - || htmlTag.IndexOf $"<{t}>" = 0 + || htmlTag.IndexOf $"<%s{t}>" = 0 || htmlTag.IndexOf $"<{t} " = 0 || htmlTag.IndexOf $" not @@ -60,7 +62,7 @@ let stripTags allowedTags input = /// Wrap a string at the specified number of characters -let wordWrap charPerLine (input : string) = +let wordWrap charPerLine (input: string) = match input.Length with | len when len <= charPerLine -> input | _ -> @@ -92,7 +94,7 @@ let wordWrap charPerLine (input : string) = |> String.concat "\n" /// Modify the text returned by CKEditor into the format we need for request and announcement text -let ckEditorToText (text : string) = +let ckEditorToText (text: string) = [ "\n\t", "" " ", " " " ", "  " @@ -119,8 +121,8 @@ let htmlToPlainText html = |> String.replace "\u00a0" " " /// Get the second portion of a tuple as a string -let sndAsString x = (snd >> string) x - +let sndAsString x = + (snd >> string) x /// Make a URL with query string parameters let makeUrl url qs = diff --git a/src/PrayerTracker.UI/ViewModels.fs b/src/PrayerTracker.UI/ViewModels.fs index c9d74e2..05008a5 100644 --- a/src/PrayerTracker.UI/ViewModels.fs +++ b/src/PrayerTracker.UI/ViewModels.fs @@ -10,14 +10,14 @@ open PrayerTracker.Entities module ReferenceList = /// A localized list of the AsOfDateDisplay DU cases - let asOfDateList (s : IStringLocalizer) = [ + let asOfDateList (s: IStringLocalizer) = [ AsOfDateDisplay.toCode NoDisplay, s["Do not display the “as of” date"] AsOfDateDisplay.toCode ShortDate, s["Display a short “as of” date"] AsOfDateDisplay.toCode LongDate, s["Display a full “as of” date"] ] /// A list of e-mail type options - let emailTypeList def (s : IStringLocalizer) = + let emailTypeList def (s: IStringLocalizer) = // Localize the default type let defaultType = s[match def with HtmlFormat -> "HTML Format" | PlainTextFormat -> "Plain-Text Format"].Value @@ -28,14 +28,14 @@ module ReferenceList = } /// A list of expiration options - let expirationList (s : IStringLocalizer) includeExpireNow = [ + let expirationList (s: IStringLocalizer) includeExpireNow = [ Expiration.toCode Automatic, s["Expire Normally"] Expiration.toCode Manual, s["Request Never Expires"] if includeExpireNow then Expiration.toCode Forced, s["Expire Immediately"] ] /// A list of request types - let requestTypeList (s : IStringLocalizer) = [ + let requestTypeList (s: IStringLocalizer) = [ CurrentRequest, s["Current Requests"] LongTermRequest, s["Long-Term Requests"] PraiseReport, s["Praise Reports"] @@ -63,7 +63,7 @@ module MessageLevel = | Warning -> "WARNING" | Error -> "ERROR" - let toCssClass level = (toString level).ToLowerInvariant () + let toCssClass level = (toString level).ToLowerInvariant() /// This is used to create a message that is displayed to the user @@ -217,7 +217,7 @@ type AssignGroups = module AssignGroups = /// Create an instance of this form from an existing user - let fromUser (user : User) = + let fromUser (user: User) = { UserId = shortGuid user.Id.Value UserName = user.Name SmallGroups = "" @@ -265,7 +265,7 @@ with member this.IsNew = emptyGuid = this.ChurchId /// Populate a church from this form - member this.PopulateChurch (church : Church) = + member this.PopulateChurch (church: Church) = { church with Name = this.Name City = this.City @@ -278,7 +278,7 @@ with module EditChurch = /// Create an instance from an existing church - let fromChurch (church : Church) = + let fromChurch (church: Church) = { ChurchId = shortGuid church.Id.Value Name = church.Name City = church.City @@ -322,7 +322,7 @@ with module EditMember = /// Create an instance from an existing member - let fromMember (mbr : Member) = + let fromMember (mbr: Member) = { MemberId = shortGuid mbr.Id.Value Name = mbr.Name Email = mbr.Email @@ -404,7 +404,7 @@ type EditPreferences = with /// Set the properties of a small group based on the form's properties - member this.PopulatePreferences (prefs : ListPreferences) = + member this.PopulatePreferences (prefs: ListPreferences) = let isPublic, grpPw = if this.Visibility = GroupVisibility.PublicList then true, "" elif this.Visibility = GroupVisibility.HasPassword then false, (defaultArg this.GroupPassword "") @@ -432,7 +432,7 @@ with /// Support for the EditPreferences type module EditPreferences = /// Populate an edit form from existing preferences - let fromPreferences (prefs : ListPreferences) = + let fromPreferences (prefs: ListPreferences) = let setType (x : string) = match x.StartsWith "#" with true -> "RGB" | false -> "Name" { ExpireDays = prefs.DaysToExpire DaysToKeepNew = prefs.DaysToKeepNew @@ -504,7 +504,7 @@ module EditRequest = } /// Create an instance from an existing request - let fromRequest (req : PrayerRequest) = + let fromRequest (req: PrayerRequest) = { empty with RequestId = shortGuid req.Id.Value RequestType = PrayerRequestType.toCode req.RequestType @@ -532,7 +532,7 @@ with member this.IsNew = emptyGuid = this.SmallGroupId /// Populate a small group from this form - member this.populateGroup (grp : SmallGroup) = + member this.populateGroup (grp: SmallGroup) = { grp with Name = this.Name ChurchId = idFromShort ChurchId this.ChurchId @@ -542,7 +542,7 @@ with module EditSmallGroup = /// Create an instance from an existing small group - let fromGroup (grp : SmallGroup) = + let fromGroup (grp: SmallGroup) = { SmallGroupId = shortGuid grp.Id.Value Name = grp.Name ChurchId = shortGuid grp.ChurchId.Value @@ -586,7 +586,7 @@ with member this.IsNew = emptyGuid = this.UserId /// Populate a user from the form - member this.PopulateUser (user : User) hasher = + member this.PopulateUser (user: User) hasher = { user with FirstName = this.FirstName LastName = this.LastName @@ -612,7 +612,7 @@ module EditUser = } /// Create an instance from an existing user - let fromUser (user : User) = + let fromUser (user: User) = { empty with UserId = shortGuid user.Id.Value FirstName = user.FirstName @@ -756,13 +756,13 @@ type RequestList = with /// Group requests by their type, along with the type and its localized string - member this.RequestsByType (s : IStringLocalizer) = + member this.RequestsByType (s: IStringLocalizer) = ReferenceList.requestTypeList s |> List.map (fun (typ, name) -> let sort = match this.SmallGroup.Preferences.RequestSort with - | SortByDate -> Seq.sortByDescending (fun req -> req.UpdatedDate) - | SortByRequestor -> Seq.sortBy (fun req -> req.Requestor) + | SortByDate -> Seq.sortByDescending _.UpdatedDate + | SortByRequestor -> Seq.sortBy _.Requestor let reqs = this.Requests |> Seq.ofList @@ -773,12 +773,12 @@ with |> List.filter (fun (_, _, reqs) -> not (List.isEmpty reqs)) /// Is this request new? - member this.IsNew (req : PrayerRequest) = + member this.IsNew (req: PrayerRequest) = let reqDate = req.UpdatedDate.InZone(SmallGroup.timeZone this.SmallGroup).Date Period.Between(reqDate, this.Date, PeriodUnits.Days).Days <= this.SmallGroup.Preferences.DaysToKeepNew /// Generate this list as HTML - member this.AsHtml (s : IStringLocalizer) = + member this.AsHtml (s: IStringLocalizer) = let p = this.SmallGroup.Preferences let asOfSize = Math.Round (float p.TextFontSize * 0.8, 2) [ if this.ShowHeader then @@ -822,8 +822,8 @@ with | LongDate -> let dt = match p.AsOfDateDisplay with - | ShortDate -> req.UpdatedDate.InZone(tz).Date.ToString ("d", null) - | LongDate -> req.UpdatedDate.InZone(tz).Date.ToString ("D", null) + | ShortDate -> req.UpdatedDate.InZone(tz).Date.ToString("d", null) + | LongDate -> req.UpdatedDate.InZone(tz).Date.ToString("D", null) | _ -> "" i [ _style $"font-size:%.2f{asOfSize}pt" ] [ rawText "  ("; str s["as of"].Value; str " "; str dt; rawText ")" @@ -835,17 +835,17 @@ with |> RenderView.AsString.htmlNodes /// Generate this list as plain text - member this.AsText (s : IStringLocalizer) = + member this.AsText (s: IStringLocalizer) = let tz = SmallGroup.timeZone this.SmallGroup seq { this.SmallGroup.Name s["Prayer Requests"].Value - this.Date.ToString (s["MMMM d, yyyy"].Value, null) + this.Date.ToString(s["MMMM d, yyyy"].Value, null) " " for _, name, reqs in this.RequestsByType s do let dashes = String.replicate (name.Value.Length + 4) "-" dashes - $" {name.Value.ToUpper ()}" + $" {name.Value.ToUpper()}" dashes for req in reqs do let bullet = if this.IsNew req then "+" else "-" @@ -855,8 +855,8 @@ with | _ -> let dt = match this.SmallGroup.Preferences.AsOfDateDisplay with - | ShortDate -> req.UpdatedDate.InZone(tz).Date.ToString ("d", null) - | LongDate -> req.UpdatedDate.InZone(tz).Date.ToString ("D", null) + | ShortDate -> req.UpdatedDate.InZone(tz).Date.ToString("d", null) + | LongDate -> req.UpdatedDate.InZone(tz).Date.ToString("D", null) | _ -> "" $""" ({s["as of"].Value} {dt})""" |> sprintf " %s %s%s%s" bullet requestor (htmlToPlainText req.Text) diff --git a/src/PrayerTracker/App.fs b/src/PrayerTracker/App.fs index 3d8011b..6910569 100644 --- a/src/PrayerTracker/App.fs +++ b/src/PrayerTracker/App.fs @@ -5,7 +5,7 @@ open Microsoft.AspNetCore.Http /// Middleware to add the starting ticks for the request type RequestStartMiddleware (next: RequestDelegate) = - member this.InvokeAsync (ctx: HttpContext) = task { + member this.InvokeAsync(ctx: HttpContext) = task { ctx.Items[Key.startTime] <- ctx.Now return! next.Invoke ctx } @@ -45,14 +45,14 @@ module Configure = open PrayerTracker.Data /// Configure ASP.NET Core's service collection (dependency injection container) - let services (svc : IServiceCollection) = + let services (svc: IServiceCollection) = let _ = svc.AddOptions() let _ = svc.AddLocalization(fun options -> options.ResourcesPath <- "Resources") let _ = svc.Configure(fun (opts: RequestLocalizationOptions) -> - let supportedCultures = [| - CultureInfo "en-US"; CultureInfo "en-GB"; CultureInfo "en-AU"; CultureInfo "en" - CultureInfo "es-MX"; CultureInfo "es-ES"; CultureInfo "es" |] + let supportedCultures = + [| CultureInfo "en-US"; CultureInfo "en-GB"; CultureInfo "en-AU"; CultureInfo "en" + CultureInfo "es-MX"; CultureInfo "es-ES"; CultureInfo "es" |] opts.DefaultRequestCulture <- RequestCulture("en-US", "en-US") opts.SupportedCultures <- supportedCultures opts.SupportedUICultures <- supportedCultures) @@ -190,7 +190,7 @@ module Configure = open Microsoft.Extensions.Options /// Configure the application - let app (app : IApplicationBuilder) = + let app (app: IApplicationBuilder) = let env = app.ApplicationServices.GetRequiredService() if env.IsDevelopment() then app.UseDeveloperExceptionPage() diff --git a/src/PrayerTracker/Extensions.fs b/src/PrayerTracker/Extensions.fs index 8c50073..4cd30fb 100644 --- a/src/PrayerTracker/Extensions.fs +++ b/src/PrayerTracker/Extensions.fs @@ -56,19 +56,18 @@ type ClaimsPrincipal with /// The ID of the currently logged on small group member this.SmallGroupId = - if this.HasClaim (fun c -> c.Type = ClaimTypes.GroupSid) then - Some (idFromShort SmallGroupId (this.FindFirst(fun c -> c.Type = ClaimTypes.GroupSid).Value)) - else None + this.FindFirstValue ClaimTypes.GroupSid + |> Option.ofObj + |> Option.map (idFromShort SmallGroupId) - /// The ID of the currently signed in user + /// The ID of the currently signed-in user member this.UserId = - if this.HasClaim (fun c -> c.Type = ClaimTypes.NameIdentifier) then - Some (idFromShort UserId (this.FindFirst(fun c -> c.Type = ClaimTypes.NameIdentifier).Value)) - else None + this.FindFirstValue ClaimTypes.NameIdentifier + |> Option.ofObj + |> Option.map (idFromShort UserId) open Giraffe -open Npgsql /// Extensions on the ASP.NET Core HTTP context type HttpContext with @@ -83,7 +82,7 @@ type HttpContext with member _.Strings = Views.I18N.localizer.Force() /// The currently logged on small group (sets the value in the session if it is missing) - member this.CurrentGroup () = task { + member this.CurrentGroup() = task { match this.Session.CurrentGroup with | Some group -> return Some group | None -> @@ -98,7 +97,7 @@ type HttpContext with } /// The currently logged on user (sets the value in the session if it is missing) - member this.CurrentUser () = task { + member this.CurrentUser() = task { match this.Session.CurrentUser with | Some user -> return Some user | None -> diff --git a/src/PrayerTracker/wwwroot/css/app.css b/src/PrayerTracker/wwwroot/_/app.css similarity index 100% rename from src/PrayerTracker/wwwroot/css/app.css rename to src/PrayerTracker/wwwroot/_/app.css diff --git a/src/PrayerTracker/wwwroot/js/app.js b/src/PrayerTracker/wwwroot/_/app.js similarity index 100% rename from src/PrayerTracker/wwwroot/js/app.js rename to src/PrayerTracker/wwwroot/_/app.js diff --git a/src/PrayerTracker/wwwroot/_/fixi-0.5.7.js b/src/PrayerTracker/wwwroot/_/fixi-0.5.7.js new file mode 100644 index 0000000..0b868fc --- /dev/null +++ b/src/PrayerTracker/wwwroot/_/fixi-0.5.7.js @@ -0,0 +1,87 @@ +(()=>{ + let send = (elt, type, detail, bub)=>elt.dispatchEvent(new CustomEvent("fx:" + type, {detail, cancelable:true, bubbles:bub !== false, composed:true})) + let attr = (elt, name, defaultVal)=>elt.getAttribute(name) || defaultVal + let ignore = (elt)=>elt.matches("[fx-ignore]") || elt.closest("[fx-ignore]") != null + let init = (elt)=>{ + let options = {} + if (elt.__fixi || ignore(elt) || !send(elt, "init", {options})) return + elt.__fixi = async(evt)=>{ + let reqs = elt.__fixi.requests ||= new Set() + let form = elt.form || elt.closest("form") + let body = new FormData(form ?? undefined, evt.submitter) + if (!form && elt.name) body.append(elt.name, elt.value) + let ac = new AbortController() + let cfg = { + trigger:evt, + action:attr(elt, "fx-action"), + method:attr(elt, "fx-method", "GET").toUpperCase(), + target: document.querySelector(attr(elt, "fx-target")) ?? elt, + swap:attr(elt, "fx-swap", "outerHTML"), + body, + drop:reqs.size, + headers:{"FX-Request":"true"}, + abort:ac.abort.bind(ac), + signal:ac.signal, + preventTrigger:true, + transition:document.startViewTransition?.bind(document), + fetch:fetch.bind(window) + } + let go = send(elt, "config", {cfg, requests:reqs}) + if (cfg.preventTrigger) evt.preventDefault() + if (!go || cfg.drop) return + if (/GET|DELETE/.test(cfg.method)){ + let params = new URLSearchParams(cfg.body) + if (params.size) + cfg.action += (/\?/.test(cfg.action) ? "&" : "?") + params + cfg.body = null + } + reqs.add(cfg) + try { + if (cfg.confirm){ + let result = await cfg.confirm() + if (!result) return + } + if (!send(elt, "before", {cfg, requests:reqs})) return + cfg.response = await cfg.fetch(cfg.action, cfg) + cfg.text = await cfg.response.text() + if (!send(elt, "after", {cfg})) return + } catch(error) { + send(elt, "error", {cfg, error}) + return + } finally { + reqs.delete(cfg) + send(elt, "finally", {cfg}) + } + let doSwap = ()=>{ + if (cfg.swap instanceof Function) + return cfg.swap(cfg) + else if (/(before|after)(start|end)/.test(cfg.swap)) + cfg.target.insertAdjacentHTML(cfg.swap, cfg.text) + else if(cfg.swap in cfg.target) + cfg.target[cfg.swap] = cfg.text + else throw cfg.swap + } + if (cfg.transition) + await cfg.transition(doSwap).finished + else + await doSwap() + send(elt, "swapped", {cfg}) + } + elt.__fixi.evt = attr(elt, "fx-trigger", elt.matches("form") ? "submit" : elt.matches("input:not([type=button]),select,textarea") ? "change" : "click") + elt.addEventListener(elt.__fixi.evt, elt.__fixi, options) + send(elt, "inited", {}, false) + } + let process = (elt)=>{ + if (elt instanceof Element){ + if (ignore(elt)) return + if (elt.matches("[fx-action]")) init(elt) + elt.querySelectorAll("[fx-action]").forEach(init) + } + } + document.addEventListener("fx:process", (evt)=>process(evt.target)) + document.addEventListener("DOMContentLoaded", ()=>{ + document.__fixi_mo = new MutationObserver((recs)=>recs.forEach((r)=>r.type === "childList" && r.addedNodes.forEach((n)=>process(n)))) + document.__fixi_mo.observe(document.body, {childList:true, subtree:true}) + process(document.body) + }) +})() diff --git a/src/PrayerTracker/wwwroot/css/help.css b/src/PrayerTracker/wwwroot/_/help.css similarity index 100% rename from src/PrayerTracker/wwwroot/css/help.css rename to src/PrayerTracker/wwwroot/_/help.css