WIP on ?chapters handler for posts (#6)

- Do not show drafts if users are not logged on
- Migrate user management to Giraffe View Engine templates
This commit is contained in:
2024-03-10 21:07:30 -04:00
parent 641a7499cc
commit 81039579ea
11 changed files with 316 additions and 330 deletions

View File

@@ -76,19 +76,27 @@ let raw = rawText
/// The pattern for a long date
let longDatePattern =
InstantPattern.CreateWithInvariantCulture "MMMM d, yyyy"
ZonedDateTimePattern.CreateWithInvariantCulture("MMMM d, yyyy", DateTimeZoneProviders.Tzdb)
/// Create a long date
let longDate =
longDatePattern.Format >> txt
let longDate app (instant: Instant) =
DateTimeZoneProviders.Tzdb[app.WebLog.TimeZone]
|> Option.ofObj
|> Option.map (fun tz -> longDatePattern.Format(instant.InZone(tz)))
|> Option.defaultValue "--"
|> txt
/// The pattern for a short time
let shortTimePattern =
InstantPattern.CreateWithInvariantCulture "h:mmtt"
ZonedDateTimePattern.CreateWithInvariantCulture("h:mmtt", DateTimeZoneProviders.Tzdb)
/// Create a short time
let shortTime instant =
txt (shortTimePattern.Format(instant).ToLower())
let shortTime app (instant: Instant) =
DateTimeZoneProviders.Tzdb[app.WebLog.TimeZone]
|> Option.ofObj
|> Option.map (fun tz -> shortTimePattern.Format(instant.InZone(tz)).ToLowerInvariant())
|> Option.defaultValue "--"
|> txt
/// Functions for generating content in varying layouts
module Layout =

View File

@@ -5,7 +5,130 @@ open Giraffe.ViewEngine.Htmx
open MyWebLog
open MyWebLog.ViewModels
/// Page to display the log on page
/// User edit form
let edit (model: EditUserModel) app =
let levelOption value name =
option [ _value value; if model.AccessLevel = value then _selected ] [ txt name ]
div [ _class "col-12" ] [
h5 [ _class "my-3" ] [ txt app.PageTitle ]
form [ _hxPost (relUrl app "admin/settings/user/save"); _method "post"; _class "container"
_hxTarget "#userList"; _hxSwap "outerHTML show:window:top" ] [
antiCsrf app
input [ _type "hidden"; _name "Id"; _value model.Id ]
div [ _class "row" ] [
div [ _class "col-12 col-md-5 col-lg-3 col-xxl-2 offset-xxl-1 mb-3" ] [
div [ _class "form-floating" ] [
select [ _name "AccessLevel"; _id "accessLevel"; _class "form-control"; _required
_autofocus ] [
levelOption (string Author) "Author"
levelOption (string Editor) "Editor"
levelOption (string WebLogAdmin) "Web Log Admin"
if app.IsAdministrator then levelOption (string Administrator) "Administrator"
]
label [ _for "accessLevel" ] [ raw "Access Level" ]
]
]
div [ _class "col-12 col-md-7 col-lg-4 col-xxl-3 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "email"; _name "Email"; _id "email"; _class "form-control"; _placeholder "E-mail"
_required; _value model.Email ]
label [ _for "email" ] [ raw "E-mail Address" ]
]
]
div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "Url"; _id "url"; _class "form-control"; _placeholder "URL"
_value model.Url ]
label [ _for "url" ] [ raw "User’s Personal URL" ]
]
]
]
div [ _class "row mb-3" ] [
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 offset-xl-1 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"
_placeholder "First"; _required; _value model.FirstName ]
label [ _for "firstName" ] [ raw "First Name" ]
]
]
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"
_placeholder "Last"; _required; _value model.LastName ]
label [ _for "lastName" ] [ raw "Last Name" ]
]
]
div [ _class "col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-0 col-xl-3 offset-xl-1 pb-3" ] [
div [ _class "form-floating " ] [
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
_placeholder "Preferred"; _required; _value model.PreferredName ]
label [ _for "preferredName" ] [ raw "Preferred Name" ]
]
]
]
div [ _class "row mb-3" ] [
div [ _class "col-12 col-xl-10 offset-xl-1" ] [
fieldset [ _class "p-2" ] [
legend [ _class "ps-1" ] [
if not model.IsNew then raw "Change "
raw "Password"
]
if not model.IsNew then
div [ _class "row" ] [
div [ _class "col" ] [
p [ _class "form-text" ] [
raw "Optional; leave blank not change the user’s password"
]
]
]
div [ _class "row" ] [
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _name "Password"; _id "password"; _class "form-control"
_placeholder "Password"
if model.IsNew then _required ]
label [ _for "password" ] [
if not model.IsNew then raw "New "
raw "Password"
]
]
]
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _name "PasswordConfirm"; _id "passwordConfirm"
_class "form-control"; _placeholder "Confirm"
if model.IsNew then _required ]
label [ _for "passwordConfirm" ] [
raw "Confirm"
if not model.IsNew then raw " New"
raw " Password"
]
]
]
]
]
]
]
div [ _class "row mb-3" ] [
div [ _class "col text-center" ] [
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]; raw "   "
if model.IsNew then
button [ _type "button"; _class "btn btn-sm btn-secondary ms-3"
_onclick "document.getElementById('user_new').innerHTML = ''" ] [
raw "Cancel"
]
else
a [ _href (relUrl app "admin/settings/users"); _class "btn btn-sm btn-secondary ms-3" ] [
raw "Cancel"
]
]
]
]
]
|> List.singleton
/// User log on form
let logOn (model: LogOnModel) (app: AppViewContext) = [
h2 [ _class "my-3" ] [ rawText "Log On to "; encodedText app.WebLog.Name ]
article [ _class "py-3" ] [
@@ -36,6 +159,7 @@ let logOn (model: LogOnModel) (app: AppViewContext) = [
]
]
/// The list of users for a web log (part of web log settings page)
let userList (model: WebLogUser list) app =
let badge = "ms-2 badge bg"
@@ -47,7 +171,7 @@ let userList (model: WebLogUser list) app =
antiCsrf app
for user in model do
div [ _class "row mwl-table-detail"; _id $"user_{user.Id}" ] [
div [ _class $"col-12 col-md-4 col-xl-3 no-wrap" ] [
div [ _class "col-12 col-md-4 col-xl-3 no-wrap" ] [
txt user.PreferredName; raw " "
match user.AccessLevel with
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
@@ -56,17 +180,16 @@ let userList (model: WebLogUser list) app =
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
br []
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
let urlBase = $"admin/settings/user/{user.Id}"
let userUrl = relUrl app $"admin/settings/user/{user.Id}"
small [] [
a [ _href (relUrl app $"{urlBase}/edit"); _hxTarget $"#user_{user.Id}"
a [ _href $"{userUrl}/edit"; _hxTarget $"#user_{user.Id}"
_hxSwap $"innerHTML show:#user_{user.Id}:top" ] [
raw "Edit"
]
if app.UserId.Value <> user.Id then
let delLink = relUrl app $"{urlBase}/delete"
span [ _class "text-muted" ] [ raw " &bull; " ]
a [ _href delLink; _hxPost delLink; _class "text-danger"
_hxConfirm $"Are you sure you want to delete the user &ldquo;{user.PreferredName}&rdquo;? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
a [ _href userUrl; _hxDelete userUrl; _class "text-danger"
_hxConfirm $"Are you sure you want to delete the user {user.PreferredName}? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
raw "Delete"
]
]
@@ -79,13 +202,101 @@ let userList (model: WebLogUser list) app =
br []; txt user.Url.Value
]
]
div [ _class "d-none d-xl-block col-xl-2" ] [ longDate user.CreatedOn ]
div [ _class "d-none d-xl-block col-xl-2" ] [
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
]
div [ _class "col-12 col-md-4 col-xl-3" ] [
match user.LastSeenOn with
| Some it -> longDate it; raw " at "; shortTime it
| Some it -> longDate app it; raw " at "; shortTime app it
| None -> raw "--"
]
]
]
]
|> List.singleton
|> List.singleton
/// Edit My Info form
let myInfo (model: EditMyInfoModel) (user: WebLogUser) app = [
h2 [ _class "my-3" ] [ txt app.PageTitle ]
article [] [
form [ _action (relUrl app "admin/my-info"); _method "post" ] [
antiCsrf app
div [ _class "d-flex flex-row flex-wrap justify-content-around" ] [
div [ _class "text-center mb-3 lh-sm" ] [
strong [ _class "text-decoration-underline" ] [ raw "Access Level" ]; br []
raw (string user.AccessLevel)
]
div [ _class "text-center mb-3 lh-sm" ] [
strong [ _class "text-decoration-underline" ] [ raw "Created" ]; br []
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
]
div [ _class "text-center mb-3 lh-sm" ] [
strong [ _class "text-decoration-underline" ] [ raw "Last Log On" ]; br []
longDate app user.LastSeenOn.Value; raw " at "; shortTime app user.LastSeenOn.Value
]
]
div [ _class "container" ] [
div [ _class "row" ] [ div [ _class "col" ] [ hr [ _class "mt-0" ] ] ]
div [ _class "row mb-3" ] [
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"; _autofocus
_required; _placeholder "First"; _value model.FirstName ]
label [ _for "firstName" ] [ raw "First Name" ]
]
]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"; _required
_placeholder "Last"; _value model.LastName ]
label [ _for "lastName" ] [ raw "Last Name" ]
]
]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
_required; _placeholder "Preferred"; _value model.PreferredName ]
label [ _for "preferredName" ] [ raw "Preferred Name" ]
]
]
]
div [ _class "row mb-3" ] [
div [ _class "col" ] [
fieldset [ _class "p-2" ] [
legend [ _class "ps-1" ] [ raw "Change Password" ]
div [ _class "row" ] [
div [ _class "col" ] [
p [ _class "form-text" ] [
raw "Optional; leave blank to keep your current password"
]
]
]
div [ _class "row" ] [
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _name "NewPassword"; _id "newPassword"
_class "form-control"; _placeholder "Password" ]
label [ _for "newPassword" ] [ raw "New Password" ]
]
]
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _name "NewPasswordConfirm"; _id "newPasswordConfirm"
_class "form-control"; _placeholder "Confirm" ]
label [ _for "newPasswordConfirm" ] [ raw "Confirm New Password" ]
]
]
]
]
]
]
div [ _class "row" ] [
div [ _class "col text-center mb-3" ] [
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
]
]
]
]
]
]