diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs index 6b962e5..e0f61ef 100644 --- a/src/MyWebLog/Handlers/Admin.fs +++ b/src/MyWebLog/Handlers/Admin.fs @@ -531,48 +531,43 @@ module WebLog = // GET /admin/settings let settings : HttpHandler = fun next ctx -> task { let data = ctx.Data - match! TemplateCache.get adminTheme "user-list-body" data with - | Ok userTemplate -> - match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with - | Ok tagMapTemplate -> - let! allPages = data.Page.All ctx.WebLog.Id - let! themes = data.Theme.All() - let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id - let! hash = - hashForPage "Web Log Settings" - |> withAntiCsrf ctx - |> addToHash ViewContext.Model (SettingsModel.FromWebLog ctx.WebLog) - |> addToHash "pages" ( - seq { - KeyValuePair.Create("posts", "- First Page of Posts -") - yield! allPages - |> List.sortBy _.Title.ToLower() - |> List.map (fun p -> KeyValuePair.Create(string p.Id, p.Title)) - } - |> Array.ofSeq) - |> addToHash "themes" ( - themes - |> Seq.ofList - |> Seq.map (fun it -> - KeyValuePair.Create(string it.Id, $"{it.Name} (v{it.Version})")) - |> Array.ofSeq) - |> addToHash "upload_values" [| - KeyValuePair.Create(string Database, "Database") - KeyValuePair.Create(string Disk, "Disk") - |] - |> addToHash "users" (users |> List.map (DisplayUser.FromUser ctx.WebLog) |> Array.ofList) - |> addToHash "rss_model" (EditRssModel.FromRssOptions ctx.WebLog.Rss) - |> addToHash "custom_feeds" ( - ctx.WebLog.Rss.CustomFeeds - |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx)) - |> Array.ofList) - |> addViewContext ctx - let! hash' = TagMapping.withTagMappings ctx hash - return! - addToHash "user_list" (userTemplate.Render hash') hash' - |> addToHash "tag_mapping_list" (tagMapTemplate.Render hash') - |> adminView "settings" next ctx - | Error message -> return! Error.server message next ctx + match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with + | Ok tagMapTemplate -> + let! allPages = data.Page.All ctx.WebLog.Id + let! themes = data.Theme.All() + let! hash = + hashForPage "Web Log Settings" + |> withAntiCsrf ctx + |> addToHash ViewContext.Model (SettingsModel.FromWebLog ctx.WebLog) + |> addToHash "pages" ( + seq { + KeyValuePair.Create("posts", "- First Page of Posts -") + yield! allPages + |> List.sortBy _.Title.ToLower() + |> List.map (fun p -> KeyValuePair.Create(string p.Id, p.Title)) + } + |> Array.ofSeq) + |> addToHash "themes" ( + themes + |> Seq.ofList + |> Seq.map (fun it -> + KeyValuePair.Create(string it.Id, $"{it.Name} (v{it.Version})")) + |> Array.ofSeq) + |> addToHash "upload_values" [| + KeyValuePair.Create(string Database, "Database") + KeyValuePair.Create(string Disk, "Disk") + |] + |> addToHash "rss_model" (EditRssModel.FromRssOptions ctx.WebLog.Rss) + |> addToHash "custom_feeds" ( + ctx.WebLog.Rss.CustomFeeds + |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx)) + |> Array.ofList) + |> addViewContext ctx + let! hash' = TagMapping.withTagMappings ctx hash + return! + hash' + |> addToHash "tag_mapping_list" (tagMapTemplate.Render hash') + |> adminView "settings" next ctx | Error message -> return! Error.server message next ctx } diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index ad3b611..7f01db7 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -381,7 +381,9 @@ let adminPage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) : let adminBarePage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) : HttpHandler = fun next ctx -> task { let! messages = getCurrentMessages ctx let appCtx = generateViewContext pageTitle messages includeCsrf ctx - return! htmlString (Layout.bare content appCtx |> RenderView.AsString.htmlDocument) next ctx + return! + ( messagesToHeaders appCtx.Messages + >=> htmlString (Layout.bare content appCtx |> RenderView.AsString.htmlDocument)) next ctx } /// Validate the anti cross-site request forgery token in the current request diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index d735bd7..9f0a508 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -207,6 +207,23 @@ let home : HttpHandler = fun next ctx -> task { | None -> return! Error.notFound next ctx } +// GET /{post-permalink}?chapters +let chapters (post: Post) : HttpHandler = + match post.Episode with + | Some ep -> + match ep.Chapters with + | Some chapters -> + + json chapters + | None -> + match ep.ChapterFile with + | Some file -> redirectTo true file + | None -> Error.notFound + | None -> Error.notFound + + +// ~~ ADMINISTRATION ~~ + // GET /admin/posts // GET /admin/posts/page/{pageNbr} let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task { @@ -372,7 +389,7 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu } // GET /admin/post/{id}/chapters -let chapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> task { +let manageChapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> task { match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with | Some post when Option.isSome post.Episode diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 1729db9..7f6429c 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -29,10 +29,15 @@ module CatchAll = match data.Post.FindByPermalink permalink webLog.Id |> await with | Some post -> debug (fun () -> "Found post by permalink") - let hash = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 data |> await - yield fun next ctx -> - addToHash ViewContext.PageTitle post.Title hash - |> themedView (defaultArg post.Template "single-post") next ctx + if post.Status = Published || Option.isSome ctx.UserAccessLevel then + if ctx.Request.Query.ContainsKey "chapters" then + yield Post.chapters post + else + yield fun next ctx -> + Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 data + |> await + |> addToHash ViewContext.PageTitle post.Title + |> themedView (defaultArg post.Template "single-post") next ctx | None -> () // Current page match data.Page.FindByPermalink permalink webLog.Id |> await with @@ -130,7 +135,7 @@ let router : HttpHandler = choose [ routef "/%s/revision/%s/preview" Post.previewRevision routef "/%s/revisions" Post.editRevisions routef "/%s/chapter/%i" Post.editChapter - routef "/%s/chapters" Post.chapters + routef "/%s/chapters" Post.manageChapters ]) subRoute "/settings" (requireAccess WebLogAdmin >=> choose [ route "" >=> Admin.WebLog.settings @@ -201,10 +206,7 @@ let router : HttpHandler = choose [ route "/save" >=> Admin.TagMapping.save routef "/%s/delete" Admin.TagMapping.delete ]) - subRoute "/user" (choose [ - route "/save" >=> User.save - routef "/%s/delete" User.delete - ]) + route "/user/save" >=> User.save ]) subRoute "/theme" (choose [ route "/new" >=> Admin.Theme.save @@ -220,6 +222,9 @@ let router : HttpHandler = choose [ subRoute "/post" (choose [ routef "/%s/chapter/%i" Post.deleteChapter ]) + subRoute "/settings" (requireAccess WebLogAdmin >=> choose [ + routef "/user/%s" User.delete + ]) ] ]) GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts diff --git a/src/MyWebLog/Handlers/User.fs b/src/MyWebLog/Handlers/User.fs index b360df5..fbd1734 100644 --- a/src/MyWebLog/Handlers/User.fs +++ b/src/MyWebLog/Handlers/User.fs @@ -5,7 +5,6 @@ open System open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Identity open MyWebLog -open NodaTime // ~~ LOG ON / LOG OFF ~~ @@ -84,7 +83,6 @@ let logOff : HttpHandler = fun next ctx -> task { // ~~ ADMINISTRATION ~~ -open System.Collections.Generic open Giraffe.Htmx /// Got no time for URL/form manipulators... @@ -98,16 +96,7 @@ let all : HttpHandler = fun next ctx -> task { /// Show the edit user page let private showEdit (model: EditUserModel) : HttpHandler = fun next ctx -> - hashForPage (if model.IsNew then "Add a New User" else "Edit User") - |> withAntiCsrf ctx - |> addToHash ViewContext.Model model - |> addToHash "access_levels" [| - KeyValuePair.Create(string Author, "Author") - KeyValuePair.Create(string Editor, "Editor") - KeyValuePair.Create(string WebLogAdmin, "Web Log Admin") - if ctx.HasAccessLevel Administrator then KeyValuePair.Create(string Administrator, "Administrator") - |] - |> adminBareView "user-edit" next ctx + adminBarePage (if model.IsNew then "Add a New User" else "Edit User") true (Views.User.edit model) next ctx // GET /admin/settings/user/{id}/edit let edit usrId : HttpHandler = fun next ctx -> task { @@ -121,7 +110,7 @@ let edit usrId : HttpHandler = fun next ctx -> task { | None -> return! Error.notFound next ctx } -// POST /admin/settings/user/{id}/delete +// DELETE /admin/settings/user/{id} let delete userId : HttpHandler = fun next ctx -> task { let data = ctx.Data match! data.WebLogUser.FindById (WebLogUserId userId) ctx.WebLog.Id with @@ -144,21 +133,11 @@ let delete userId : HttpHandler = fun next ctx -> task { | None -> return! Error.notFound next ctx } -/// Display the user "my info" page, with information possibly filled in -let private showMyInfo (model: EditMyInfoModel) (user: WebLogUser) : HttpHandler = fun next ctx -> - hashForPage "Edit Your Information" - |> withAntiCsrf ctx - |> addToHash ViewContext.Model model - |> addToHash "access_level" (string user.AccessLevel) - |> addToHash "created_on" (ctx.WebLog.LocalTime user.CreatedOn) - |> addToHash "last_seen_on" (ctx.WebLog.LocalTime (defaultArg user.LastSeenOn (Instant.FromUnixTimeSeconds 0))) - |> adminView "my-info" next ctx - - // GET /admin/my-info let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task { match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with - | Some user -> return! showMyInfo (EditMyInfoModel.FromUser user) user next ctx + | Some user -> + return! adminPage "Edit Your Information" true (Views.User.myInfo (EditMyInfoModel.FromUser user) user) next ctx | None -> return! Error.notFound next ctx } @@ -181,7 +160,10 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task { return! redirectToGet "admin/my-info" next ctx | Some user -> do! addMessage ctx { UserMessage.Error with Message = "Passwords did not match; no updates made" } - return! showMyInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user next ctx + return! + adminPage + "Edit Your Information" true + (Views.User.myInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user) next ctx | None -> return! Error.notFound next ctx } diff --git a/src/MyWebLog/Views/Helpers.fs b/src/MyWebLog/Views/Helpers.fs index 0c90f15..3e8f5d6 100644 --- a/src/MyWebLog/Views/Helpers.fs +++ b/src/MyWebLog/Views/Helpers.fs @@ -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 = diff --git a/src/MyWebLog/Views/User.fs b/src/MyWebLog/Views/User.fs index d395825..dedfab2 100644 --- a/src/MyWebLog/Views/User.fs +++ b/src/MyWebLog/Views/User.fs @@ -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 " • " ] - a [ _href delLink; _hxPost delLink; _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.)" ] [ + 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 \ No newline at end of file + |> 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" ] + ] + ] + ] + ] + ] +] diff --git a/src/admin-theme/my-info.liquid b/src/admin-theme/my-info.liquid deleted file mode 100644 index f38e3ad..0000000 --- a/src/admin-theme/my-info.liquid +++ /dev/null @@ -1,74 +0,0 @@ -