Fix request search (#55); WIP on full htmx (#56)

- Removed fixi library (not a good fit)
This commit is contained in:
Daniel J. Summers 2025-02-01 15:52:31 -05:00
parent 2e5a1426f6
commit c9ccfe8b68
6 changed files with 104 additions and 93 deletions

View File

@ -68,6 +68,10 @@ module Json =
opts opts
module private Helpers =
let instant (it: Instant) =
it.ToString()
open BitBadger.Documents open BitBadger.Documents
open BitBadger.Documents.Sqlite open BitBadger.Documents.Sqlite
@ -120,19 +124,19 @@ module SmallGroups =
/// Query to retrieve data for a small group info instance /// Query to retrieve data for a small group info instance
let private infoQuery = let private infoQuery =
$"SELECT g.data->>'id' AS id, g.data->>'groupName' AS groupName, c.data->>'churchName' AS churchName, $"SELECT g.data->>'id' AS id, g.data->>'name' AS groupName, c.data->>'name' AS churchName,
g.data->'preferences'->>'timeZoneId' AS timeZoneId, g.data->'preferences'->>'isPublic' AS isPublic g.data->'preferences'->>'timeZoneId' AS timeZoneId, g.data->'preferences'->>'isPublic' AS isPublic
FROM {Table.Group} g FROM {Table.Group} g
INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'"
/// Query to retrieve data for a small group select list item /// Query to retrieve data for a small group select list item
let private itemQuery = let private itemQuery =
$"SELECT g.data->>'groupName' AS groupName, g.data->>'id' AS id, c.data->>'churchName' AS churchName $"SELECT g.data->>'name' AS groupName, g.data->>'id' AS id, c.data->>'name' AS churchName
FROM {Table.Group} g FROM {Table.Group} g
INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'"
/// The ORDER BY clause for select list item queries /// The ORDER BY clause for select list item queries
let private itemOrderBy = "ORDER BY c.data->>'churchName', g.data->>'groupName'" let private itemOrderBy = "ORDER BY c.data->>'name', g.data->>'name'"
/// Map a row to a Small Group list item /// Map a row to a Small Group list item
let private toSmallGroupItem (rdr: SqliteDataReader) = let private toSmallGroupItem (rdr: SqliteDataReader) =
@ -142,13 +146,13 @@ module SmallGroups =
/// Get the group IDs for the given church /// Get the group IDs for the given church
let internal groupIdsByChurch (churchId: ChurchId) = let internal groupIdsByChurch (churchId: ChurchId) =
backgroundTask { backgroundTask {
let! groups = Find.byFields<SmallGroup> Table.Group All [ Field.Equal "churchId" churchId ] let! groups = Find.byFields<SmallGroup> Table.Group All [ Field.Equal "churchId" (string churchId) ]
return groups |> List.map _.Id return groups |> List.map _.Id
} }
/// Count the number of small groups for a church /// Count the number of small groups for a church
let countByChurch (churchId: ChurchId) = let countByChurch (churchId: ChurchId) =
Count.byFields Table.Group All [ Field.Equal "churchId" churchId ] Count.byFields Table.Group All [ Field.Equal "churchId" (string churchId) ]
/// Delete a small group by its ID /// Delete a small group by its ID
let deleteById (groupId: SmallGroupId) = let deleteById (groupId: SmallGroupId) =
@ -156,20 +160,21 @@ module SmallGroups =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
use! txn = conn.BeginTransactionAsync() use! txn = conn.BeginTransactionAsync()
let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] let! users =
Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ (string groupId) ] ]
for user in users do for user in users do
do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |} do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |}
do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ] do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" (string groupId) ]
do! conn.deleteById Table.Group groupId do! conn.deleteById Table.Group (string groupId)
do! txn.CommitAsync() do! txn.CommitAsync()
} }
/// Get information for all small groups /// Get information for all small groups
let infoForAll () = let infoForAll () =
Custom.list $"{infoQuery} ORDER BY g.data->>'groupName'" [] SmallGroupInfo.FromReader Custom.list $"{infoQuery} ORDER BY g.data->>'name'" [] SmallGroupInfo.FromReader
/// Get a list of small group IDs along with a description that includes the church name /// Get a list of small group IDs along with a description that includes the church name
let listAll () = let listAll () =
@ -197,14 +202,14 @@ module SmallGroups =
Find.firstByFields<SmallGroup> Find.firstByFields<SmallGroup>
Table.Group Table.Group
All All
[ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] [ Field.Equal "id" (string groupId); Field.Equal "preferences.groupPassword" password ]
/// Save a small group /// Save a small group
let save group = save<SmallGroup> Table.Group group let save group = save<SmallGroup> Table.Group group
/// Save a small group's list preferences /// Save a small group's list preferences
let savePreferences (groupId: SmallGroupId) (pref: ListPreferences) = let savePreferences (groupId: SmallGroupId) (pref: ListPreferences) =
Patch.byId Table.Group groupId {| Preferences = pref |} Patch.byId Table.Group (string groupId) {| Preferences = pref |}
/// Get a small group by its ID (including list preferences) /// Get a small group by its ID (including list preferences)
let tryById groupId = let tryById groupId =
@ -223,17 +228,18 @@ module Churches =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
use! txn = conn.BeginTransactionAsync() use! txn = conn.BeginTransactionAsync()
let! groupIds = SmallGroups.groupIdsByChurch churchId let! groupIds = SmallGroups.groupIdsByChurch churchId
let gIdStrings = groupIds |> List.map string
do! Delete.byFields Table.Request All [ Field.In "smallGroupId" groupIds ] do! Delete.byFields Table.Request All [ Field.In "smallGroupId" gIdStrings ]
let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User gIdStrings ]
for user in users do for user in users do
do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except groupIds |} do! Patch.byId Table.User (string user.Id) {| SmallGroups = user.SmallGroups |> List.except groupIds |}
do! Delete.byFields Table.Group All [ Field.Equal "churchId" churchId ] do! Delete.byFields Table.Group All [ Field.Equal "churchId" (string churchId) ]
do! Delete.byId Table.Church churchId do! Delete.byId Table.Church (string churchId)
do! txn.CommitAsync() do! txn.CommitAsync()
} }
@ -250,17 +256,17 @@ module Members =
/// Count members for the given small group /// Count members for the given small group
let countByGroup (groupId: SmallGroupId) = let countByGroup (groupId: SmallGroupId) =
Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ] Count.byFields Table.Member All [ Field.Equal "smallGroupId" (string groupId) ]
/// Delete a small group member by its ID /// Delete a small group member by its ID
let deleteById (memberId: MemberId) = Delete.byId Table.Member memberId let deleteById (memberId: MemberId) = Delete.byId Table.Member (string memberId)
/// Retrieve all members for a given small group /// Retrieve all members for a given small group
let forGroup (groupId: SmallGroupId) = let forGroup (groupId: SmallGroupId) =
Find.byFieldsOrdered<Member> Find.byFieldsOrdered<Member>
Table.Member Table.Member
All All
[ Field.Equal "smallGroupId" groupId ] [ Field.Equal "smallGroupId" (string groupId) ]
[ Field.Named "memberName" ] [ Field.Named "memberName" ]
/// Save a small group member /// Save a small group member
@ -312,15 +318,15 @@ module PrayerRequests =
let countByChurch churchId = let countByChurch churchId =
backgroundTask { backgroundTask {
let! groupIds = SmallGroups.groupIdsByChurch churchId let! groupIds = SmallGroups.groupIdsByChurch churchId
return! Count.byFields Table.Request All [ Field.In "smallGroupId" groupIds ] return! Count.byFields Table.Request All [ Field.In "smallGroupId" (List.map string groupIds) ]
} }
/// Count the number of prayer requests for a small group /// Count the number of prayer requests for a small group
let countByGroup (groupId: SmallGroupId) = let countByGroup (groupId: SmallGroupId) =
Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ] Count.byFields Table.Request All [ Field.Equal "smallGroupId" (string groupId) ]
/// Delete a prayer request by its ID /// Delete a prayer request by its ID
let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request reqId let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request (string reqId)
/// Get all (or active) requests for a small group as of now or the specified date /// Get all (or active) requests for a small group as of now or the specified date
let forGroup (opts: PrayerRequestOptions) = let forGroup (opts: PrayerRequestOptions) =
@ -332,11 +338,11 @@ module PrayerRequests =
(theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone)
- Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire)
.ToInstant() .ToInstant()
$"""AND ( data->>'updatedDate' > :updatedDate $"""AND ( date(data->>'updatedDate') > date(:updatedDate)
OR data->>'expiration' = :expManual OR data->>'expiration' = :expManual
OR data->>'requestType' IN (:typLongTerm, :typExpecting)) OR data->>'requestType' IN (:typLongTerm, :typExpecting))
AND data->>'expiration' <> :expForced""", AND data->>'expiration' <> :expForced""",
[ SqliteParameter(":updatedDate", expDate) [ SqliteParameter(":updatedDate", string expDate)
SqliteParameter(":expManual", string Manual) SqliteParameter(":expManual", string Manual)
SqliteParameter(":typLongTerm", string LongTermRequest) SqliteParameter(":typLongTerm", string LongTermRequest)
SqliteParameter(":typExpecting", string Expecting) SqliteParameter(":typExpecting", string Expecting)
@ -346,8 +352,9 @@ module PrayerRequests =
Custom.list Custom.list
$"SELECT data FROM {Table.Request} $"SELECT data FROM {Table.Request}
WHERE data->>'smallGroupId = :groupId {sql} WHERE data->>'smallGroupId' = :groupId
ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} {sql}
{orderBy opts.SmallGroup.Preferences.RequestSort}
{paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}" {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}"
(SqliteParameter(":groupId", string opts.SmallGroup.Id) :: parameters) (SqliteParameter(":groupId", string opts.SmallGroup.Id) :: parameters)
fromData<PrayerRequest> fromData<PrayerRequest>
@ -357,18 +364,20 @@ module PrayerRequests =
/// Search prayer requests for the given term /// Search prayer requests for the given term
let searchForGroup group searchTerm pageNbr = let searchForGroup group searchTerm pageNbr =
let pct = "%"
Custom.list Custom.list
$"SELECT data FROM {Table.Request} $"WITH results AS (
WHERE data->>'smallGroupId' = :groupId SELECT data FROM {Table.Request}
AND data->>'requestText' LIKE :search WHERE data->>'smallGroupId' = :groupId
UNION AND data->>'text' LIKE :search
SELECT data FROM {Table.Request} UNION
WHERE data->>'smallGroupId' = :groupId SELECT data FROM {Table.Request}
AND COALESCE(data->>'requestor', '') LIKE :search WHERE data->>'smallGroupId' = :groupId
ORDER BY {orderBy group.Preferences.RequestSort} AND COALESCE(data->>'requestor', '') LIKE :search)
SELECT data FROM results
{orderBy group.Preferences.RequestSort}
{paginate pageNbr group.Preferences.PageSize}" {paginate pageNbr group.Preferences.PageSize}"
[ SqliteParameter(":groupId", string group.Id) [ SqliteParameter(":groupId", string group.Id); SqliteParameter(":search", $"{pct}%s{searchTerm}{pct}") ]
SqliteParameter(":search", $"%%%s{searchTerm}%%") ]
fromData<PrayerRequest> fromData<PrayerRequest>
/// Retrieve a prayer request by its ID /// Retrieve a prayer request by its ID
@ -380,11 +389,11 @@ module PrayerRequests =
if withTime then if withTime then
Patch.byId Patch.byId
Table.Request Table.Request
req.Id (string req.Id)
{| UpdatedDate = req.UpdatedDate {| UpdatedDate = req.UpdatedDate
Expiration = req.Expiration |} Expiration = req.Expiration |}
else else
Patch.byId Table.Request req.Id {| Expiration = req.Expiration |} Patch.byId Table.Request (string req.Id) {| Expiration = req.Expiration |}
/// Functions to manipulate users /// Functions to manipulate users
@ -398,22 +407,22 @@ module Users =
let countByChurch churchId = let countByChurch churchId =
backgroundTask { backgroundTask {
let! groupIds = SmallGroups.groupIdsByChurch churchId let! groupIds = SmallGroups.groupIdsByChurch churchId
return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User (List.map string groupIds) ]
} }
/// Count the number of users for a small group /// Count the number of users for a small group
let countByGroup (groupId: SmallGroupId) = let countByGroup (groupId: SmallGroupId) =
Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ (string groupId) ] ]
/// Delete a user by its database ID /// Delete a user by its database ID
let deleteById (userId: UserId) = Delete.byId Table.User userId let deleteById (userId: UserId) = Delete.byId Table.User (string userId)
/// Get a list of users authorized to administer the given small group /// Get a list of users authorized to administer the given small group
let listByGroupId (groupId: SmallGroupId) = let listByGroupId (groupId: SmallGroupId) =
Find.byFieldsOrdered<User> Find.byFieldsOrdered<User>
Table.User Table.User
All All
[ Field.InArray "smallGroups" Table.User [ groupId ] ] [ Field.InArray "smallGroups" Table.User [ (string groupId) ] ]
[ Field.Named "lastName"; Field.Named "firstName" ] [ Field.Named "lastName"; Field.Named "firstName" ]
/// Save a user's information /// Save a user's information
@ -425,7 +434,7 @@ module Users =
Table.User Table.User
All All
[ Field.Equal "email" email [ Field.Equal "email" email
Field.InArray "smallGroups" Table.User [ groupId ] ] Field.InArray "smallGroups" Table.User [ (string groupId) ] ]
/// Find a user by their database ID /// Find a user by their database ID
let tryById userId = let tryById userId =
@ -433,12 +442,12 @@ module Users =
/// Update a user's last seen date/time /// Update a user's last seen date/time
let updateLastSeen (userId: UserId) (now: Instant) = let updateLastSeen (userId: UserId) (now: Instant) =
Patch.byId Table.User userId {| LastSeen = now |} Patch.byId Table.User (string userId) {| LastSeen = now |}
/// Update a user's password hash /// Update a user's password hash
let updatePassword (user: User) = let updatePassword (user: User) =
Patch.byId Table.User user.Id {| PasswordHash = user.PasswordHash |} Patch.byId Table.User (string user.Id) {| PasswordHash = user.PasswordHash |}
/// Update a user's authorized small groups /// Update a user's authorized small groups
let updateSmallGroups (userId: UserId) (groupIds: SmallGroupId list) = let updateSmallGroups (userId: UserId) (groupIds: SmallGroupId list) =
Patch.byId Table.User userId {| SmallGroups = groupIds |} Patch.byId Table.User (string userId) {| SmallGroups = groupIds |}

View File

@ -87,6 +87,7 @@ module Configure =
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SessionDB")) "./data/session.db" let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SessionDB")) "./data/session.db"
let _ = svc.AddSqliteCache(fun o -> o.CachePath <- cachePath) let _ = svc.AddSqliteCache(fun o -> o.CachePath <- cachePath)
let _ = svc.AddSession() let _ = svc.AddSession()
let _ = svc.AddLogging()
let _ = svc.AddAntiforgery() let _ = svc.AddAntiforgery()
let _ = svc.AddRouting() let _ = svc.AddRouting()
let _ = svc.AddSingleton<IClock> SystemClock.Instance let _ = svc.AddSingleton<IClock> SystemClock.Instance
@ -219,8 +220,8 @@ module Configure =
open Microsoft.Extensions.Options open Microsoft.Extensions.Options
/// Configure the application /// Configure the application
let app (app: IApplicationBuilder) = let app (app: WebApplication) =
let env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>() let env = app.Services.GetRequiredService<IWebHostEnvironment>()
if env.IsDevelopment() then if env.IsDevelopment() then
app.UseDeveloperExceptionPage() app.UseDeveloperExceptionPage()
@ -232,24 +233,20 @@ module Configure =
let _ = app.UseCanonicalDomains() let _ = app.UseCanonicalDomains()
let _ = app.UseStatusCodePagesWithReExecute "/error/{0}" let _ = app.UseStatusCodePagesWithReExecute "/error/{0}"
let _ = app.UseStaticFiles() let _ = app.UseStaticFiles()
let _ = app.UseCookiePolicy(CookiePolicyOptions(MinimumSameSitePolicy = SameSiteMode.Strict))
let _ =
app.UseCookiePolicy(CookiePolicyOptions(MinimumSameSitePolicy = SameSiteMode.Strict))
let _ = app.UseMiddleware<RequestStartMiddleware>() let _ = app.UseMiddleware<RequestStartMiddleware>()
let _ = app.UseRouting() let _ = app.UseRouting()
let _ = app.UseSession() let _ = app.UseSession()
let _ = app.UseRequestLocalization(app.Services.GetService<IOptions<RequestLocalizationOptions>>().Value)
let _ =
app.UseRequestLocalization(app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>().Value)
let _ = app.UseAuthentication() let _ = app.UseAuthentication()
let _ = app.UseAuthorization() let _ = app.UseAuthorization()
let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints routes) let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints routes)
app.ApplicationServices.GetRequiredService<IStringLocalizerFactory>() app.Services.GetRequiredService<IStringLocalizerFactory>()
|> Views.I18N.setUpFactories |> Views.I18N.setUpFactories
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
/// The web application /// The web application
module App = module App =
@ -258,22 +255,32 @@ module App =
[<EntryPoint>] [<EntryPoint>]
let main args = let main args =
let contentRoot = Directory.GetCurrentDirectory()
let app = let contentRoot = Directory.GetCurrentDirectory()
WebHostBuilder() let builder =
.UseContentRoot(contentRoot) WebApplication.CreateBuilder(
WebApplicationOptions(
Args = args,
ApplicationName = "PrayerTracker",
ContentRootPath = contentRoot,
WebRootPath = Path.Combine(contentRoot, "wwwroot")))
let _ =
builder.WebHost
.ConfigureAppConfiguration(Configure.configuration) .ConfigureAppConfiguration(Configure.configuration)
.UseKestrel(Configure.kestrel) .ConfigureKestrel(Configure.kestrel)
.UseWebRoot(Path.Combine(contentRoot, "wwwroot"))
.ConfigureServices(Configure.services) .ConfigureServices(Configure.services)
.ConfigureLogging(Configure.logging) .ConfigureLogging(Configure.logging)
.Configure(System.Action<IApplicationBuilder> Configure.app)
.Build()
if args.Length > 0 then use app = builder.Build()
printfn $"Unrecognized option {args[0]}"
else Configure.app app
app.Run()
let fac = app.Services.GetRequiredService<ILoggerFactory>()
let log = fac.CreateLogger "PrayerTracker"
log.LogInformation "Application Started"
app.Run()
log.LogInformation "Application Shutting Down"
0 0

View File

@ -13,11 +13,11 @@ let toSelectList<'T> valFunc textFunc withDefault emptyText (items: 'T seq) =
SelectListItem($"""&mdash; %A{s[emptyText]} &mdash;""", "") SelectListItem($"""&mdash; %A{s[emptyText]} &mdash;""", "")
| _ -> () | _ -> ()
yield! items |> Seq.map (fun x -> SelectListItem(textFunc x, valFunc x)) ] yield! items |> Seq.map (fun x -> SelectListItem(textFunc x, valFunc x)) ]
/// Create a select list from an enumeration /// Create a select list from an enumeration
let toSelectListWithEmpty<'T> valFunc textFunc emptyText (items: 'T seq) = let toSelectListWithEmpty<'T> valFunc textFunc emptyText (items: 'T seq) =
toSelectList valFunc textFunc true emptyText items toSelectList valFunc textFunc true emptyText items
/// Create a select list from an enumeration /// Create a select list from an enumeration
let toSelectListWithDefault<'T> valFunc textFunc (items: 'T seq) = let toSelectListWithDefault<'T> valFunc textFunc (items: 'T seq) =
toSelectList valFunc textFunc true "Select" items toSelectList valFunc textFunc true "Select" items
@ -117,7 +117,7 @@ let addInfo ctx msg =
/// Add an informational HTML message to the session /// Add an informational HTML message to the session
let addHtmlInfo ctx msg = let addHtmlInfo ctx msg =
addUserMessage ctx { UserMessage.info with Text = htmlString msg } addUserMessage ctx { UserMessage.info with Text = htmlString msg }
/// Add a warning message to the session /// Add a warning message to the session
let addWarning ctx msg = let addWarning ctx msg =
addUserMessage ctx { UserMessage.warning with Text = htmlLocString msg } addUserMessage ctx { UserMessage.warning with Text = htmlLocString msg }

View File

@ -182,13 +182,6 @@ let toHtmlIds it =
let renderHtmlNode = RenderView.AsString.htmlNode 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 open Microsoft.AspNetCore.Html
/// Render an HTML node, then return the value as an HTML string /// Render an HTML node, then return the value as an HTML string
@ -224,6 +217,10 @@ module TimeZones =
open Giraffe.ViewEngine.Htmx open Giraffe.ViewEngine.Htmx
/// Create a page link that will make the request with fixi
let pageLink href attrs content =
a (List.append [ _href href; _hxGet href ] attrs) content
/// Known htmx targets /// Known htmx targets
module Target = module Target =

View File

@ -12,7 +12,7 @@ let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" e
/// Navigation items /// Navigation items
module Navigation = module Navigation =
/// Top navigation bar /// Top navigation bar
let top m = let top m =
let s = I18N.localizer.Force() let s = I18N.localizer.Force()
@ -103,7 +103,7 @@ module Navigation =
section [ _class "pt-title-bar-center"; _ariaLabel "Empty center space in top menu" ] [] 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" ] [ section [ _class "pt-title-bar-right"; _roleToolBar; _ariaLabel "Right side of top menu" ] [
ul [] rightLinks ] ] ul [] rightLinks ] ]
/// Identity bar (below top nav) /// Identity bar (below top nav)
let identity m = let identity m =
let s = I18N.localizer.Force() let s = I18N.localizer.Force()
@ -111,7 +111,7 @@ module Navigation =
div [] [ div [] [
span [ _title s["Language"].Value ] [ icon "record_voice_over"; space ] span [ _title s["Language"].Value ] [ icon "record_voice_over"; space ]
match langCode () with match langCode () with
| "es" -> | "es" ->
strong [] [ locStr s["Spanish"] ] strong [] [ locStr s["Spanish"] ]
rawText " &nbsp; &nbsp; " rawText " &nbsp; &nbsp; "
pageLink "/language/en" [] [ locStr s["Change to English"] ] pageLink "/language/en" [] [ locStr s["Change to English"] ]
@ -142,14 +142,14 @@ module Navigation =
/// Content layouts /// Content layouts
module Content = module Content =
/// Content layout that tops at 60rem /// Content layout that tops at 60rem
let standard = div [ _class "pt-content" ] let standard = div [ _class "pt-content" ]
/// Content layout that uses the full width of the browser window /// Content layout that uses the full width of the browser window
let wide = div [ _class "pt-content pt-full-width" ] let wide = div [ _class "pt-content pt-full-width" ]
/// Separator for parts of the title /// Separator for parts of the title
let private titleSep = rawText " &#xab; " let private titleSep = rawText " &#xab; "
@ -274,14 +274,14 @@ let private partialHead pgTitle =
let private pageLayout viewInfo pgTitle content = let private pageLayout viewInfo pgTitle content =
body [] [ body [] [
Navigation.top viewInfo Navigation.top viewInfo
div [ _id "pt-body" ] (contentSection viewInfo pgTitle content) div [ _id "pt-body"; Target.content ] (contentSection viewInfo pgTitle content)
match viewInfo.Layout with match viewInfo.Layout with
| FullPage -> | FullPage ->
script [ _src "/js/ckeditor/ckeditor.js" ] [] script [ _src "/js/ckeditor/ckeditor.js" ] []
script [ _src "/_/fixi-0.5.7.js" ] [] Htmx.Script.minified
script [ _src "/_/app.js" ] [] script [ _src "/_/app.js" ] []
| _ -> () ] | _ -> () ]
/// The standard layout(s) for PrayerTracker /// The standard layout(s) for PrayerTracker
let standard viewInfo pageTitle content = let standard viewInfo pageTitle content =
let s = I18N.localizer.Force() let s = I18N.localizer.Force()
@ -328,7 +328,7 @@ let help pageTitle isHome content =
div [] [ div [] [
locStr s["Language"]; rawText ": " locStr s["Language"]; rawText ": "
match langCode () with match langCode () with
| "es" -> | "es" ->
locStr s["Spanish"]; rawText " &bull; " locStr s["Spanish"]; rawText " &bull; "
a [ _href "/language/en" ] [ locStr s["Change to English"] ] a [ _href "/language/en" ] [ locStr s["Change to English"] ]
| _ -> | _ ->
@ -348,4 +348,3 @@ let help pageTitle isHome content =
p [ _class "pt-center-text" ] [ p [ _class "pt-center-text" ] [
a [ _href "/help"; _title s["Help Index"].Value ] [ a [ _href "/help"; _title s["Help Index"].Value ] [
rawText "&#xab; "; locStr s["Back to Help Index"] ] ] ] ] ] ] ] rawText "&#xab; "; locStr s["Back to Help Index"] ] ] ] ] ] ] ]

View File

@ -15,7 +15,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe.Fixi" Version="0.5.7" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="2.0.4" /> <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="2.0.4" />
<PackageReference Include="MailKit" Version="4.10.0" /> <PackageReference Include="MailKit" Version="4.10.0" />