Use grid layout for tables (#38)

- Add icon for current language
This commit is contained in:
Daniel J. Summers 2022-08-06 19:36:02 -04:00
parent 57814b5bf1
commit 980346b98d
6 changed files with 206 additions and 196 deletions

View File

@ -2,6 +2,7 @@
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open PrayerTracker
open PrayerTracker.Entities
open PrayerTracker.ViewModels
@ -59,70 +60,48 @@ let edit (model : EditChurch) ctx viewInfo =
/// View for church maintenance page
let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx viewInfo =
let s = I18N.localizer.Force ()
let vi =
AppViewInfo.withScopedStyles [
"#churchList { grid-template-columns: repeat(7, auto); }"
] viewInfo
let chTbl =
let vi = AppViewInfo.withScopedStyles [ "#churchList { grid-template-columns: repeat(7, auto); }" ] viewInfo
let churchTable =
match churches with
| [] -> space
| _ ->
section [ _id "churchList"; _class "pt-data-list"; _ariaLabel "Church list" ] [
header [] [ locStr s["Actions"] ]
header [] [ locStr s["Name"] ]
header [] [ locStr s["Location"] ]
header [] [ locStr s["Groups"] ]
header [] [ locStr s["Requests"] ]
header [] [ locStr s["Users"] ]
header [] [ locStr s["Interface?"] ]
section [ _id "churchList"; _class "pt-table"; _ariaLabel "Church list" ] [
div [ _class "row head" ] [
header [ _class "cell" ] [ locStr s["Actions"] ]
header [ _class "cell" ] [ locStr s["Name"] ]
header [ _class "cell" ] [ locStr s["Location"] ]
header [ _class "cell" ] [ locStr s["Groups"] ]
header [ _class "cell" ] [ locStr s["Requests"] ]
header [ _class "cell" ] [ locStr s["Users"] ]
header [ _class "cell" ] [ locStr s["Interface?"] ]
]
for church in churches do
let churchId = shortGuid church.Id.Value
let delAction = $"/church/{churchId}/delete"
let delPrompt = s["Are you sure you want to delete this {0}? This action cannot be undone.",
$"""{s["Church"].Value.ToLower ()} ({church.Name})"""]
div [ _class "row" ] [
div [] [
a [ _href $"/church/{churchId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ]
div [ _class "cell actions" ] [
a [ _href $"/church/{churchId}/edit"; _title s["Edit This Church"].Value ] [
iconSized 18 "edit"
]
a [ _href delAction
_title s["Delete This Church"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
_hxPost delAction
_hxConfirm delPrompt.Value ] [
iconSized 18 "delete_forever"
]
]
div [] [ str church.Name ]
div [] [ str church.City; rawText ", "; str church.State ]
div [ _class "pt-right-text" ] [ rawText (stats[churchId].SmallGroups.ToString "N0") ]
div [ _class "pt-right-text" ] [ rawText (stats[churchId].PrayerRequests.ToString "N0") ]
div [ _class "pt-right-text" ] [ rawText (stats[churchId].Users.ToString "N0") ]
div [ _class "pt-center-text" ] [ locStr s[if church.HasVpsInterface then "Yes" else "No"] ]
div [ _class "cell" ] [ str church.Name ]
div [ _class "cell" ] [ str church.City; rawText ", "; str church.State ]
div [ _class "cell pt-right-text" ] [ rawText (stats[churchId].SmallGroups.ToString "N0") ]
div [ _class "cell pt-right-text" ] [ rawText (stats[churchId].PrayerRequests.ToString "N0") ]
div [ _class "cell pt-right-text" ] [ rawText (stats[churchId].Users.ToString "N0") ]
div [ _class "cell pt-center-text" ] [
locStr s[if church.HasVpsInterface then "Yes" else "No"]
]
]
]
// table [ _class "pt-table pt-action-table" ] [
// tableHeadings s [ "Actions"; "Name"; "Location"; "Groups"; "Requests"; "Users"; "Interface?" ]
// churches
// |> List.map (fun ch ->
// let chId = shortGuid ch.Id.Value
// let delAction = $"/church/{chId}/delete"
// let delPrompt = s["Are you sure you want to delete this {0}? This action cannot be undone.",
// $"""{s["Church"].Value.ToLower ()} ({ch.Name})"""]
// tr [] [
// td [] [
// a [ _href $"/church/{chId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ]
// a [ _href delAction
// _title s["Delete This Church"].Value
// _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
// icon "delete_forever"
// ]
// ]
// td [] [ str ch.Name ]
// td [] [ str ch.City; rawText ", "; str ch.State ]
// td [ _class "pt-right-text" ] [ rawText (stats[chId].SmallGroups.ToString "N0") ]
// td [ _class "pt-right-text" ] [ rawText (stats[chId].PrayerRequests.ToString "N0") ]
// td [ _class "pt-right-text" ] [ rawText (stats[chId].Users.ToString "N0") ]
// td [ _class "pt-center-text" ] [ locStr s[if ch.HasVpsInterface then "Yes" else "No"] ]
// ])
// |> tbody []
// ]
[ div [ _class "pt-center-text" ] [
br []
a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ] [
@ -132,8 +111,10 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vie
br []
]
tableSummary churches.Length s
chTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
form [ _method "post" ] [
csrfToken ctx
churchTable
]
]
|> Layout.Content.wide
|> Layout.standard vi "Maintain Churches"

View File

@ -141,15 +141,15 @@ module Navigation =
let s = I18N.localizer.Force ()
header [ _id "pt-language"; Target.body ] [
div [] [
span [ _class "u" ] [ locStr s["Language"]; rawText ": " ]
span [ _title s["Language"].Value ] [ icon "record_voice_over"; space ]
match langCode () with
| "es" ->
locStr s["Spanish"]
rawText " &nbsp; &bull; &nbsp; "
strong [] [ locStr s["Spanish"] ]
rawText " &nbsp; &nbsp; "
a [ _href "/language/en" ] [ locStr s["Change to English"] ]
| _ ->
locStr s["English"]
rawText " &nbsp; &bull; &nbsp; "
strong [] [ locStr s["English"] ]
rawText " &nbsp; &nbsp; "
a [ _href "/language/es" ] [ locStr s["Cambie a Español"] ]
]
match m.Group with

View File

@ -4,6 +4,8 @@ open System
open System.IO
open Giraffe
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open Microsoft.AspNetCore.Http
open NodaTime
open PrayerTracker
@ -156,10 +158,11 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
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 ""
if req.UpdateRequired now prefs.DaysToExpire prefs.LongTermUpdateWeeks then "cell pt-request-update" else "cell"
|> _class
let reqExp (req : PrayerRequest) =
_class (if req.IsExpired now prefs.DaysToExpire then "pt-request-expired" else "")
_class (if req.IsExpired now prefs.DaysToExpire then "cell pt-request-expired" else "cell")
let vi = AppViewInfo.withScopedStyles [ "#requestList { grid-template-columns: repeat(5, auto); }" ] viewInfo
/// Iterate the sequence once, before we render, so we can get the count of it at the top of the table
let requests =
model.Requests
@ -175,33 +178,34 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
.Value
]
|> String.concat ""
tr [] [
td [] [
div [ _class "row" ] [
div [ _class "cell actions" ] [
a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] [
icon "edit"
iconSized 18 "edit"
]
if req.IsExpired now prefs.DaysToExpire then
a [ _href $"/prayer-request/{reqId}/restore"
_title l["Restore This Inactive Request"].Value ] [
icon "visibility"
iconSized 18 "visibility"
]
else
a [ _href $"/prayer-request/{reqId}/expire"
_title l["Expire This Request Immediately"].Value ] [
icon "visibility_off"
iconSized 18 "visibility_off"
]
a [ _href delAction
_title l["Delete This Request"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
_hxPost delAction
_hxConfirm delPrompt ] [
iconSized 18 "delete_forever"
]
]
td [ updReq req ] [
div [ updReq req ] [
str (req.UpdatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture))
]
td [] [ locStr types[req.RequestType] ]
td [ reqExp req ] [ str (match req.Requestor with Some r -> r | None -> " ") ]
td [] [
div [ _class "cell" ] [ locStr types[req.RequestType] ]
div [ reqExp req ] [ str (match req.Requestor with Some r -> r | None -> " ") ]
div [ _class "cell" ] [
match reqText.Length with
| len when len < 60 -> rawText reqText
| _ -> rawText $"{reqText[0..59]}&hellip;"
@ -235,9 +239,18 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
match requests.Length with
| 0 -> ()
| _ ->
table [ _class "pt-table pt-action-table" ] [
tableHeadings s [ "Actions"; "Updated Date"; "Type"; "Requestor"; "Request"]
tbody [] requests
form [ _method "post" ] [
csrfToken ctx
section [ _id "requestList"; _class "pt-table"; _ariaLabel "Prayer request list" ] [
div [ _class "row head" ] [
header [ _class "cell" ] [ locStr s["Actions"] ]
header [ _class "cell" ] [ locStr s["Updated Date"] ]
header [ _class "cell" ] [ locStr s["Type"] ]
header [ _class "cell" ] [ locStr s["Requestor"] ]
header [ _class "cell" ] [ locStr s["Request"] ]
]
yield! requests
]
]
div [ _class "pt-center-text" ] [
br []
@ -272,10 +285,9 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
]
| false -> ()
]
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
]
|> Layout.Content.wide
|> Layout.standard viewInfo (match model.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests")
|> Layout.standard vi (match model.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests")
/// View for the printable prayer request list

View File

@ -1,6 +1,8 @@
module PrayerTracker.Views.SmallGroup
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Entities
@ -180,32 +182,39 @@ let logOn (groups : SmallGroup list) grpId ctx viewInfo =
/// View for the small group maintenance page
let maintain (groups : SmallGroup list) ctx viewInfo =
let s = I18N.localizer.Force ()
let grpTbl =
let vi = AppViewInfo.withScopedStyles [ "#groupList { grid-template-columns: repeat(4, auto); }" ] viewInfo
let groupTable =
match groups with
| [] -> space
| _ ->
table [ _class "pt-table pt-action-table" ] [
tableHeadings s [ "Actions"; "Name"; "Church"; "Time Zone"]
groups
|> List.map (fun g ->
let grpId = shortGuid g.Id.Value
section [ _id "groupList"; _class "pt-table"; _ariaLabel "Small group list" ] [
div [ _class "row head" ] [
header [ _class "cell" ] [ locStr s["Actions"] ]
header [ _class "cell" ] [ locStr s["Name"] ]
header [ _class "cell" ] [ locStr s["Church"] ]
header [ _class "cell" ] [ locStr s["Time Zone"] ]
]
for group in groups do
let grpId = shortGuid group.Id.Value
let delAction = $"/small-group/{grpId}/delete"
let delPrompt = s["Are you sure you want to delete this {0}? This action cannot be undone.",
$"""{s["Small Group"].Value.ToLower ()} ({g.Name})""" ].Value
tr [] [
td [] [
a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] [ icon "edit" ]
$"""{s["Small Group"].Value.ToLower ()} ({group.Name})""" ].Value
div [ _class "row" ] [
div [ _class "cell actions" ] [
a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] [
iconSized 18 "edit"
]
a [ _href delAction
_title s["Delete This Group"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
_hxDelete delAction
_hxConfirm delPrompt ] [
iconSized 18 "delete_forever"
]
]
td [] [ str g.Name ]
td [] [ str g.Church.Name ]
td [] [ locStr (TimeZones.name g.Preferences.TimeZoneId s) ]
])
|> tbody []
div [ _class "cell" ] [ str group.Name ]
div [ _class "cell" ] [ str group.Church.Name ]
div [ _class "cell" ] [ locStr (TimeZones.name group.Preferences.TimeZoneId s) ]
]
]
[ div [ _class "pt-center-text" ] [
br []
@ -216,45 +225,54 @@ let maintain (groups : SmallGroup list) ctx viewInfo =
br []
]
tableSummary groups.Length s
grpTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
form [ _method "post" ] [
csrfToken ctx
groupTable
]
]
|> Layout.Content.standard
|> Layout.standard viewInfo "Maintain Groups"
|> Layout.standard vi "Maintain Groups"
/// View for the member maintenance page
let members (members : Member list) (emailTypes : Map<string, LocalizedString>) ctx viewInfo =
let s = I18N.localizer.Force ()
let mbrTbl =
let vi = AppViewInfo.withScopedStyles [ "#memberList { grid-template-columns: repeat(4, auto); }" ] viewInfo
let memberTable =
match members with
| [] -> space
| _ ->
table [ _class "pt-table pt-action-table" ] [
tableHeadings s [ "Actions"; "Name"; "E-mail Address"; "Format"]
members
|> List.map (fun mbr ->
section [ _id "memberList"; _class "pt-table"; _ariaLabel "Small group member list" ] [
div [ _class "row head" ] [
header [ _class "cell"] [ locStr s["Actions"] ]
header [ _class "cell"] [ locStr s["Name"] ]
header [ _class "cell"] [ locStr s["E-mail Address"] ]
header [ _class "cell"] [ locStr s["Format"] ]
]
for mbr in members do
let mbrId = shortGuid mbr.Id.Value
let delAction = $"/small-group/member/{mbrId}/delete"
let delPrompt =
s["Are you sure you want to delete this {0}? This action cannot be undone.", s["group member"]]
.Value.Replace("?", $" ({mbr.Name})?")
tr [] [
td [] [
div [ _class "row" ] [
div [ _class "cell actions" ] [
a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ] [
icon "edit"
iconSized 18 "edit"
]
a [ _href delAction
_title s["Delete This Group Member"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
_hxPost delAction
_hxConfirm delPrompt ] [
iconSized 18 "delete_forever"
]
]
div [ _class "cell" ] [ str mbr.Name ]
div [ _class "cell" ] [ str mbr.Email ]
div [ _class "cell" ] [
locStr emailTypes[defaultArg (mbr.Format |> Option.map EmailFormat.toCode) ""]
]
]
td [] [ str mbr.Name ]
td [] [ str mbr.Email ]
td [] [ locStr emailTypes[defaultArg (mbr.Format |> Option.map EmailFormat.toCode) ""] ]
])
|> tbody []
]
[ div [ _class"pt-center-text" ] [
br []
@ -265,15 +283,15 @@ let members (members : Member list) (emailTypes : Map<string, LocalizedString>)
br []
]
tableSummary members.Length s
mbrTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
form [ _method "post" ] [
csrfToken ctx
memberTable
]
]
|> Layout.Content.standard
|> Layout.standard viewInfo "Maintain Group Members"
|> Layout.standard vi "Maintain Group Members"
open Giraffe.ViewEngine.Accessibility
/// View for the small group overview page
let overview model viewInfo =
let s = I18N.localizer.Force ()

View File

@ -1,6 +1,8 @@
module PrayerTracker.Views.User
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open PrayerTracker
open PrayerTracker.ViewModels
@ -8,37 +10,33 @@ open PrayerTracker.ViewModels
let assignGroups model groups curGroups ctx viewInfo =
let s = I18N.localizer.Force ()
let pageTitle = sprintf "%s %A" model.UserName s["Assign Groups"]
let vi = AppViewInfo.withScopedStyles [ "#groupList { grid-template-columns: auto; }" ] viewInfo
form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx
inputField "hidden" (nameof model.UserId) model.UserId []
inputField "hidden" (nameof model.UserName) model.UserName []
table [ _class "pt-table" ] [
thead [] [
tr [] [
th [] [ rawText "&nbsp;" ]
th [] [ locStr s["Group"] ]
section [ _id "groupList"; _class "pt-table"; _ariaLabel "Assigned small groups" ] [
div [ _class "row head" ] [
header [ _class "cell" ] [ locStr s["Group"] ]
]
]
groups
|> List.map (fun (grpId, grpName) ->
let inputId = $"id-{grpId}"
tr [] [
td [] [
for groupId, name in groups do
div [ _class "row" ] [
div [ _class "cell" ] [
input [ _type "checkbox"
_name (nameof model.SmallGroups)
_id inputId
_value grpId
if List.contains grpId curGroups then _checked ]
_id groupId
_value groupId
if List.contains groupId curGroups then _checked ]
space
label [ _for groupId ] [ str name ]
]
]
td [] [ label [ _for inputId ] [ str grpName ] ]
])
|> tbody []
]
div [ _fieldRow ] [ submit [] "save" s["Save Group Assignments"] ]
]
|> List.singleton
|> Layout.Content.standard
|> Layout.standard viewInfo pageTitle
|> Layout.standard vi pageTitle
/// View for the password change page
@ -187,39 +185,44 @@ open PrayerTracker.Entities
/// View for the user maintenance page
let maintain (users : User list) ctx viewInfo =
let s = I18N.localizer.Force ()
let usrTbl =
let vi = AppViewInfo.withScopedStyles [ "#userList { grid-template-columns: repeat(4, auto); }" ] viewInfo
let userTable =
match users with
| [] -> space
| _ ->
table [ _class "pt-table pt-action-table" ] [
tableHeadings s [ "Actions"; "Name"; "Last Seen"; "Admin?" ]
users
|> List.map (fun user ->
section [ _id "userList"; _class "pt-table"; _ariaLabel "User list" ] [
div [ _class "row head" ] [
header [ _class "cell" ] [ locStr s["Actions"] ]
header [ _class "cell" ] [ locStr s["Name"] ]
header [ _class "cell" ] [ locStr s["Last Seen"] ]
header [ _class "cell" ] [ locStr s["Admin?"] ]
]
for user in users do
let userId = shortGuid user.Id.Value
let delAction = $"/user/{userId}/delete"
let delPrompt = s["Are you sure you want to delete this {0}? This action cannot be undone.",
$"""{s["User"].Value.ToLower ()} ({user.Name})"""].Value
tr [] [
td [] [
a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ icon "edit" ]
div [ _class "row" ] [
div [ _class "cell actions" ] [
a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ iconSized 18 "edit" ]
a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ] [
icon "group"
iconSized 18 "group"
]
a [ _href delAction
_title s["Delete This User"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
_hxPost delAction
_hxConfirm delPrompt ] [
iconSized 18 "delete_forever"
]
]
td [] [ str user.Name ]
td [] [
div [ _class "cell" ] [ str user.Name ]
div [ _class "cell" ] [
str (match user.LastSeen with Some dt -> dt.ToString s["MMMM d, yyyy"] | None -> "--")
]
td [ _class "pt-center-text" ] [
div [ _class "cell pt-center-text" ] [
if user.IsAdmin then strong [] [ locStr s["Yes"] ] else locStr s["No"]
]
])
|> tbody []
]
]
[ div [ _class "pt-center-text" ] [
br []
@ -230,8 +233,10 @@ let maintain (users : User list) ctx viewInfo =
br []
]
tableSummary users.Length s
usrTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
form [ _method "post" ] [
csrfToken ctx
userTable
]
]
|> Layout.Content.standard
|> Layout.standard viewInfo "Maintain Users"
|> Layout.standard vi "Maintain Users"

View File

@ -263,54 +263,48 @@ footer a:hover {
text-align: right;
}
.pt-data-list {
.pt-table {
display: grid;
justify-content: center;
grid-row-gap: .5rem;
grid-column-gap: 1rem;
}
.pt-data-list .row {
.pt-table .row.head,
.pt-table .row {
display: contents;
}
.pt-data-list .row:hover > * {
background-color: purple;
}
.pt-table {
margin: auto;
border-collapse: collapse;
}
.pt-table thead {
background-image: linear-gradient(to bottom, var(--dark), var(--lighter-dark));
color: white;
}
.pt-table tbody tr:hover {
.pt-table .row:hover > * {
background-color: #eee;
}
.pt-table tr th,
.pt-table tr td {
.pt-table .row.head .cell {
background-image: linear-gradient(to bottom, var(--dark), var(--lighter-dark));
font-weight: bold;
color: white;
text-align: center;
font-size: .85rem;
}
.pt-table .row.head .cell:first-of-type {
border-top-left-radius: .5rem;
}
.pt-table .row.head .cell:last-of-type {
border-top-right-radius: .5rem;
}
.pt-table .cell {
padding: .25rem .5rem;
border-bottom: dotted 1px var(--lighter-dark);
}
.pt-table tbody tr td {
border-bottom: solid 1px var(--lighter-dark);
.pt-table .cell.actions {
border-bottom-color: var(--background);
}
.pt-action-table tbody tr td:first-child {
white-space: nowrap;
border-bottom: 0;
}
.pt-action-table tbody tr td:first-child a {
.pt-table .cell.actions a {
background-color: gray;
color: white;
border-radius: .5rem;
padding: .125rem .5rem .5rem .5rem;
padding: 0 .5rem .25rem;
margin: 0 .125rem;
}
.pt-action-table tbody tr:hover td:first-child a {
.pt-table .row:hover .cell.actions a {
background-color: var(--dark);
}
.pt-action-table tbody tr td:first-child a:hover {
border-bottom: 0;
}
/* TODO: Figure out nice CSS transitions for these; these don't work */
.pt-fadeable {
height: 0;