diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs
index df5c257..2446197 100644
--- a/src/MyWebLog/Handlers/Admin.fs
+++ b/src/MyWebLog/Handlers/Admin.fs
@@ -27,35 +27,13 @@ module Dashboard =
ListedPages = listed
Categories = cats
TopLevelCategories = topCats }
- return! adminPage "Dashboard" false next ctx (Views.Admin.dashboard model)
+ return! adminPage "Dashboard" false next ctx (Views.WebLog.dashboard model)
}
// GET /admin/administration
let admin : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
- let! themes = ctx.Data.Theme.All()
- let cachedTemplates = TemplateCache.allNames ()
- return!
- hashForPage "myWebLog Administration"
- |> withAntiCsrf ctx
- |> addToHash "cached_themes" (
- themes
- |> Seq.ofList
- |> Seq.map (fun it -> [|
- string it.Id
- it.Name
- cachedTemplates
- |> List.filter _.StartsWith(string it.Id)
- |> List.length
- |> string
- |])
- |> Array.ofSeq)
- |> addToHash "web_logs" (
- WebLogCache.all ()
- |> Seq.ofList
- |> Seq.sortBy _.Name
- |> Seq.map (fun it -> [| string it.Id; it.Name; it.UrlBase |])
- |> Array.ofSeq)
- |> adminView "admin-dashboard" next ctx
+ let! themes = ctx.Data.Theme.All()
+ return! adminPage "myWebLog Administration" true next ctx (Views.Admin.dashboard themes)
}
/// Redirect the user to the admin dashboard
@@ -117,43 +95,24 @@ module Category =
open MyWebLog.Data
// GET /admin/categories
- let all : HttpHandler = fun next ctx -> task {
- match! TemplateCache.get adminTheme "category-list-body" ctx.Data with
- | Ok catListTemplate ->
- let! hash =
- hashForPage "Categories"
- |> withAntiCsrf ctx
- |> addViewContext ctx
- return!
- addToHash "category_list" (catListTemplate.Render hash) hash
- |> adminView "category-list" next ctx
- | Error message -> return! Error.server message next ctx
- }
-
- // GET /admin/categories/bare
- let bare : HttpHandler = fun next ctx ->
- hashForPage "Categories"
- |> withAntiCsrf ctx
- |> adminBareView "category-list-body" next ctx
-
+ let all : HttpHandler = fun next ctx ->
+ adminPage "Categories" true next ctx Views.WebLog.categoryList
// GET /admin/category/{id}/edit
let edit catId : HttpHandler = fun next ctx -> task {
let! result = task {
match catId with
- | "new" -> return Some("Add a New Category", { Category.Empty with Id = CategoryId "new" })
+ | "new" -> return Some ("Add a New Category", { Category.Empty with Id = CategoryId "new" })
| _ ->
match! ctx.Data.Category.FindById (CategoryId catId) ctx.WebLog.Id with
- | Some cat -> return Some("Edit Category", cat)
+ | Some cat -> return Some ("Edit Category", cat)
| None -> return None
}
match result with
| Some (title, cat) ->
return!
- hashForPage title
- |> withAntiCsrf ctx
- |> addToHash ViewContext.Model (EditCategoryModel.FromCategory cat)
- |> adminBareView "category-edit" next ctx
+ Views.WebLog.categoryEdit (EditCategoryModel.FromCategory cat)
+ |> adminBarePage title true next ctx
| None -> return! Error.notFound next ctx
}
@@ -171,16 +130,16 @@ module Category =
Name = model.Name
Slug = model.Slug
Description = if model.Description = "" then None else Some model.Description
- ParentId = if model.ParentId = "" then None else Some(CategoryId model.ParentId) }
+ ParentId = if model.ParentId = "" then None else Some (CategoryId model.ParentId) }
do! (if model.IsNew then data.Category.Add else data.Category.Update) updatedCat
do! CategoryCache.update ctx
do! addMessage ctx { UserMessage.Success with Message = "Category saved successfully" }
- return! bare next ctx
+ return! all next ctx
| None -> return! Error.notFound next ctx
}
- // POST /admin/category/{id}/delete
- let delete catId : HttpHandler = fun next ctx -> task {
+ // DELETE /admin/category/{id}
+ let delete catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! result = ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id
match result with
| CategoryDeleted
@@ -194,7 +153,7 @@ module Category =
do! addMessage ctx { UserMessage.Success with Message = "Category deleted successfully"; Detail = detail }
| CategoryNotFound ->
do! addMessage ctx { UserMessage.Error with Message = "Category not found; cannot delete" }
- return! bare next ctx
+ return! all next ctx
}
@@ -205,19 +164,20 @@ module RedirectRules =
// GET /admin/settings/redirect-rules
let all : HttpHandler = fun next ctx ->
- adminPage "Redirect Rules" true next ctx (Views.Admin.redirectList ctx.WebLog.RedirectRules)
+ adminPage "Redirect Rules" true next ctx (Views.WebLog.redirectList ctx.WebLog.RedirectRules)
// GET /admin/settings/redirect-rules/[index]
let edit idx : HttpHandler = fun next ctx ->
let titleAndView =
if idx = -1 then
- Some ("Add", Views.Admin.redirectEdit (EditRedirectRuleModel.FromRule -1 RedirectRule.Empty))
+ Some ("Add", Views.WebLog.redirectEdit (EditRedirectRuleModel.FromRule -1 RedirectRule.Empty))
else
let rules = ctx.WebLog.RedirectRules
if rules.Length < idx || idx < 0 then
None
else
- Some ("Edit", (Views.Admin.redirectEdit (EditRedirectRuleModel.FromRule idx (List.item idx rules))))
+ Some
+ ("Edit", (Views.WebLog.redirectEdit (EditRedirectRuleModel.FromRule idx (List.item idx rules))))
match titleAndView with
| Some (title, view) -> adminBarePage $"{title} Redirect Rule" true next ctx view
| None -> Error.notFound next ctx
@@ -284,7 +244,7 @@ module TagMapping =
// GET /admin/settings/tag-mappings
let all : HttpHandler = fun next ctx -> task {
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
- return! adminBarePage "Tag Mapping List" true next ctx (Views.Admin.tagMapList mappings)
+ return! adminBarePage "Tag Mapping List" true next ctx (Views.WebLog.tagMapList mappings)
}
// GET /admin/settings/tag-mapping/{id}/edit
@@ -296,7 +256,7 @@ module TagMapping =
match! tagMap with
| Some tm ->
return!
- Views.Admin.tagMapEdit (EditTagMapModel.FromMapping tm)
+ Views.WebLog.tagMapEdit (EditTagMapModel.FromMapping tm)
|> adminBarePage (if isNew then "Add Tag Mapping" else $"Mapping for {tm.Tag} Tag") true next ctx
| None -> return! Error.notFound next ctx
}
@@ -497,7 +457,7 @@ module WebLog =
let uploads = [ Database; Disk ]
let feeds = ctx.WebLog.Rss.CustomFeeds |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx))
return!
- Views.Admin.webLogSettings
+ Views.WebLog.webLogSettings
(SettingsModel.FromWebLog ctx.WebLog) themes pages uploads (EditRssModel.FromRssOptions ctx.WebLog.Rss)
feeds
|> adminPage "Web Log Settings" true next ctx
diff --git a/src/MyWebLog/Handlers/Feed.fs b/src/MyWebLog/Handlers/Feed.fs
index c1e8cdd..8c55dbd 100644
--- a/src/MyWebLog/Handlers/Feed.fs
+++ b/src/MyWebLog/Handlers/Feed.fs
@@ -2,7 +2,6 @@
module MyWebLog.Handlers.Feed
open System
-open System.Collections.Generic
open System.IO
open System.Net
open System.ServiceModel.Syndication
@@ -446,7 +445,7 @@ let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next
{ Name = string Newsletter; Value = "Newsletter" }
{ Name = string Blog; Value = "Blog" }
]
- Views.Admin.feedEdit (EditCustomFeedModel.FromFeed f) ratings mediums
+ Views.WebLog.feedEdit (EditCustomFeedModel.FromFeed f) ratings mediums
|> adminPage $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed""" true next ctx
| None -> Error.notFound next ctx
@@ -474,7 +473,7 @@ let saveCustomFeed : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
| None -> return! Error.notFound next ctx
}
-// POST /admin/settings/rss/{id}/delete
+// DELETE /admin/settings/rss/{id}
let deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data
match! data.WebLog.FindById ctx.WebLog.Id with
diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs
index 87fe4e5..733f29b 100644
--- a/src/MyWebLog/Handlers/Routes.fs
+++ b/src/MyWebLog/Handlers/Routes.fs
@@ -114,7 +114,6 @@ let router : HttpHandler = choose [
route "/administration" >=> Admin.Dashboard.admin
subRoute "/categor" (requireAccess WebLogAdmin >=> choose [
route "ies" >=> Admin.Category.all
- route "ies/bare" >=> Admin.Category.bare
routef "y/%s/edit" Admin.Category.edit
])
route "/dashboard" >=> Admin.Dashboard.user
@@ -184,11 +183,10 @@ let router : HttpHandler = choose [
routef "/%s/revision/%s/restore" Post.restoreRevision
])
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
- route "" >=> Admin.WebLog.saveSettings
+ route "" >=> Admin.WebLog.saveSettings
subRoute "/rss" (choose [
- route "" >=> Feed.saveSettings
- route "/save" >=> Feed.saveCustomFeed
- routef "/%s/delete" Feed.deleteCustomFeed
+ route "" >=> Feed.saveSettings
+ route "/save" >=> Feed.saveCustomFeed
])
subRoute "/redirect-rules" (choose [
routef "/%i" Admin.RedirectRules.save
@@ -202,13 +200,10 @@ let router : HttpHandler = choose [
route "/new" >=> Admin.Theme.save
routef "/%s/delete" Admin.Theme.delete
])
- subRoute "/upload" (choose [
- route "/save" >=> Upload.save
- routexp "/delete/(.*)" Upload.deleteFromDisk
- routef "/%s/delete" Upload.deleteFromDb
- ])
+ route "/upload/save" >=> Upload.save
]
DELETE >=> validateCsrf >=> choose [
+ routef "/category/%s" Admin.Category.delete
subRoute "/page" (choose [
routef "/%s" Page.delete
routef "/%s/revision/%s" Page.deleteRevision
@@ -221,9 +216,14 @@ let router : HttpHandler = choose [
routef "/%s/revisions" Post.purgeRevisions
])
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
- routef "/user/%s" User.delete
routef "/redirect-rules/%i" Admin.RedirectRules.delete
+ routef "/rss/%s" Feed.deleteCustomFeed
routef "/tag-mapping/%s" Admin.TagMapping.delete
+ routef "/user/%s" User.delete
+ ])
+ subRoute "/upload" (requireAccess WebLogAdmin >=> choose [
+ routexp "/disk/(.*)" Upload.deleteFromDisk
+ routef "/%s" Upload.deleteFromDb
])
]
])
diff --git a/src/MyWebLog/Handlers/Upload.fs b/src/MyWebLog/Handlers/Upload.fs
index aac82d9..a52b037 100644
--- a/src/MyWebLog/Handlers/Upload.fs
+++ b/src/MyWebLog/Handlers/Upload.fs
@@ -108,30 +108,24 @@ let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
Path = file.Replace($"{path}{slash}", "").Replace(name, "").Replace(slash, '/')
UpdatedOn = create
Source = string Disk })
- |> List.ofSeq
with
| :? DirectoryNotFoundException -> [] // This is fine
| ex ->
warn "Upload" ctx $"Encountered {ex.GetType().Name} listing uploads for {path}:\n{ex.Message}"
[]
- let allFiles =
- dbUploads
- |> List.map (DisplayUpload.FromUpload webLog Database)
- |> List.append diskUploads
- |> List.sortByDescending (fun file -> file.UpdatedOn, file.Path)
return!
- hashForPage "Uploaded Files"
- |> withAntiCsrf ctx
- |> addToHash "files" allFiles
- |> adminView "upload-list" next ctx
+ dbUploads
+ |> Seq.ofList
+ |> Seq.map (DisplayUpload.FromUpload webLog Database)
+ |> Seq.append diskUploads
+ |> Seq.sortByDescending (fun file -> file.UpdatedOn, file.Path)
+ |> Views.WebLog.uploadList
+ |> adminPage "Uploaded Files" true next ctx
}
// GET /admin/upload/new
let showNew : HttpHandler = requireAccess Author >=> fun next ctx ->
- hashForPage "Upload a File"
- |> withAntiCsrf ctx
- |> addToHash "destination" (string ctx.WebLog.Uploads)
- |> adminView "upload-new" next ctx
+ adminPage "Upload a File" true next ctx Views.WebLog.uploadNew
/// Redirect to the upload list
@@ -173,8 +167,8 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
return! RequestErrors.BAD_REQUEST "Bad request; no file present" next ctx
}
-// POST /admin/upload/{id}/delete
-let deleteFromDb upId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
+// DELETE /admin/upload/{id}
+let deleteFromDb upId : HttpHandler = fun next ctx -> task {
match! ctx.Data.Upload.Delete (UploadId upId) ctx.WebLog.Id with
| Ok fileName ->
do! addMessage ctx { UserMessage.Success with Message = $"{fileName} deleted successfully" }
@@ -193,8 +187,8 @@ let removeEmptyDirectories (webLog: WebLog) (filePath: string) =
path <- String.Join(slash, path.Split slash |> Array.rev |> Array.skip 1 |> Array.rev)
else finished <- true
-// POST /admin/upload/delete/{**path}
-let deleteFromDisk urlParts : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
+// DELETE /admin/upload/disk/{**path}
+let deleteFromDisk urlParts : HttpHandler = fun next ctx -> task {
let filePath = urlParts |> Seq.skip 1 |> Seq.head
let path = Path.Combine(uploadDir, ctx.WebLog.Slug, filePath)
if File.Exists path then
diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj
index 40b0eb6..105c91b 100644
--- a/src/MyWebLog/MyWebLog.fsproj
+++ b/src/MyWebLog/MyWebLog.fsproj
@@ -14,6 +14,7 @@
+
diff --git a/src/MyWebLog/Views/Admin.fs b/src/MyWebLog/Views/Admin.fs
index e1e171a..a3933db 100644
--- a/src/MyWebLog/Views/Admin.fs
+++ b/src/MyWebLog/Views/Admin.fs
@@ -2,493 +2,107 @@ module MyWebLog.Views.Admin
open Giraffe.Htmx.Common
open Giraffe.ViewEngine
-open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open MyWebLog
open MyWebLog.ViewModels
-/// The main dashboard
-let dashboard (model: DashboardModel) app = [
- h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " • Dashboard" ]
- article [ _class "container" ] [
- div [ _class "row" ] [
- section [ _class "col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3" ] [
- div [ _class "card" ] [
- header [ _class "card-header text-white bg-primary" ] [ raw "Posts" ]
- div [ _class "card-body" ] [
- h6 [ _class "card-subtitle text-muted pb-3" ] [
- raw "Published "
- span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Posts) ]
- raw " Drafts "
- span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Drafts) ]
- ]
- if app.IsAuthor then
- a [ _href (relUrl app "admin/posts"); _class "btn btn-secondary me-2" ] [ raw "View All" ]
- a [ _href (relUrl app "admin/post/new/edit"); _class "btn btn-primary" ] [
- raw "Write a New Post"
- ]
- ]
- ]
- ]
- section [ _class "col-lg-5 col-xl-4 pb-3" ] [
- div [ _class "card" ] [
- header [ _class "card-header text-white bg-primary" ] [ raw "Pages" ]
- div [ _class "card-body" ] [
- h6 [ _class "card-subtitle text-muted pb-3" ] [
- raw "All "
- span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Pages) ]
- raw " Shown in Page List "
- span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.ListedPages) ]
- ]
- if app.IsAuthor then
- a [ _href (relUrl app "admin/pages"); _class "btn btn-secondary me-2" ] [ raw "View All" ]
- a [ _href (relUrl app "admin/page/new/edit"); _class "btn btn-primary" ] [
- raw "Create a New Page"
- ]
- ]
+/// The administrator dashboard
+let dashboard (themes: Theme list) app = [
+ let templates = TemplateCache.allNames ()
+ let cacheBaseUrl = relUrl app "admin/cache/"
+ let webLogCacheUrl = $"{cacheBaseUrl}web-log/"
+ let themeCacheUrl = $"{cacheBaseUrl}theme/"
+ let webLogDetail (webLog: WebLog) =
+ let refreshUrl = $"{webLogCacheUrl}{webLog.Id}/refresh"
+ div [ _class "row mwl-table-detail" ] [
+ div [ _class "col" ] [
+ txt webLog.Name; br []
+ small [] [
+ span [ _class "text-muted" ] [ raw webLog.UrlBase ]; br []
+ a [ _href refreshUrl; _hxPost refreshUrl ] [ raw "Refresh" ]
]
]
]
- div [ _class "row" ] [
- section [ _class "col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3" ] [
- div [ _class "card" ] [
- header [ _class "card-header text-white bg-secondary" ] [ raw "Categories" ]
- div [ _class "card-body" ] [
- h6 [ _class "card-subtitle text-muted pb-3"] [
- raw "All "
- span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Categories) ]
- raw " Top Level "
- span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.TopLevelCategories) ]
- ]
- if app.IsWebLogAdmin then
- a [ _href (relUrl app "admin/categories"); _class "btn btn-secondary me-2" ] [
- raw "View All"
- ]
- a [ _href (relUrl app "admin/category/new/edit"); _class "btn btn-secondary" ] [
- raw "Add a New Category"
- ]
- ]
+ let themeDetail (theme: Theme) =
+ let refreshUrl = $"{themeCacheUrl}{theme.Id}/refresh"
+ div [ _class "row mwl-table-detail" ] [
+ div [ _class "col-8" ] [
+ txt theme.Name; br []
+ small [] [
+ span [ _class "text-muted" ] [ txt (string theme.Id); raw " • " ]
+ a [ _href refreshUrl; _hxPost refreshUrl ] [ raw "Refresh" ]
]
]
+ div [ _class "col-4" ] [
+ raw (templates |> List.filter _.StartsWith(string theme.Id) |> List.length |> string)
+ ]
]
- if app.IsWebLogAdmin then
- div [ _class "row pb-3" ] [
- div [ _class "col text-end" ] [
- a [ _href (relUrl app "admin/settings"); _class "btn btn-secondary" ] [ raw "Modify Settings" ]
- ]
- ]
- ]
-]
-
-/// Custom RSS feed edit form
-let feedEdit (model: EditCustomFeedModel) (ratings: MetaItem list) (mediums: MetaItem list) app = [
h2 [ _class "my-3" ] [ raw app.PageTitle ]
article [] [
- form [ _action (relUrl app "admin/settings/rss/save"); _method "post"; _class "container" ] [
- antiCsrf app
- input [ _type "hidden"; _name "Id"; _value model.Id ]
- div [ _class "row pb-3" ] [
- div [ _class "col" ] [
- a [ _href (relUrl app "admin/settings#rss-settings") ] [ raw "« Back to Settings" ]
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col-12 col-lg-6" ] [
- fieldset [ _class "container pb-0" ] [
- legend [] [ raw "Identification" ]
- div [ _class "row" ] [
- div [ _class "col" ] [
- textField [ _required ] (nameof model.Path) "Relative Feed Path" model.Path [
- span [ _class "form-text fst-italic" ] [ raw "Appended to "; txt app.WebLog.UrlBase ]
- ]
- ]
- ]
- div [ _class "row" ] [
- div [ _class "col py-3 d-flex align-self-center justify-content-center" ] [
- checkboxSwitch [ _onclick "Admin.checkPodcast()"; if model.IsPodcast then _checked ]
- (nameof model.IsPodcast) "This Is a Podcast Feed" model.IsPodcast []
- ]
- ]
- ]
- ]
- div [ _class "col-12 col-lg-6" ] [
- fieldset [ _class "container pb-0" ] [
- legend [] [ raw "Feed Source" ]
- div [ _class "row d-flex align-items-center" ] [
- div [ _class "col-1 d-flex justify-content-end pb-3" ] [
- div [ _class "form-check form-check-inline me-0" ] [
- input [ _type "radio"; _name (nameof model.SourceType); _id "SourceTypeCat"
- _class "form-check-input"; _value "category"
- if model.SourceType <> "tag" then _checked
- _onclick "Admin.customFeedBy('category')" ]
- label [ _for "SourceTypeCat"; _class "form-check-label d-none" ] [ raw "Category" ]
- ]
- ]
- div [ _class "col-11 pb-3" ] [
- let cats =
- app.Categories
- |> Seq.ofArray
- |> Seq.map (fun c ->
- let parents =
- if c.ParentNames.Length = 0 then ""
- else
- c.ParentNames
- |> Array.map (fun it -> $"{it} 〉 ")
- |> String.concat ""
- { Name = c.Id; Value = $"{parents} {c.Name}".Trim() })
- |> Seq.append [ { Name = ""; Value = "– Select Category –" } ]
- |> List.ofSeq
- selectField [ _id "SourceValueCat"; _required
- if model.SourceType = "tag" then _disabled ]
- (nameof model.SourceValue) "Category" model.SourceValue cats (_.Name)
- (_.Value) []
- ]
- div [ _class "col-1 d-flex justify-content-end pb-3" ] [
- div [ _class "form-check form-check-inline me-0" ] [
- input [ _type "radio"; _name (nameof model.SourceType); _id "SourceTypeTag"
- _class "form-check-input"; _value "tag"
- if model.SourceType= "tag" then _checked
- _onclick "Admin.customFeedBy('tag')" ]
- label [ _for "sourceTypeTag"; _class "form-check-label d-none" ] [ raw "Tag" ]
- ]
- ]
- div [ _class "col-11 pb-3" ] [
- textField [ _id "SourceValueTag"; _required
- if model.SourceType <> "tag" then _disabled ]
- (nameof model.SourceValue) "Tag"
- (if model.SourceType = "tag" then model.SourceValue else "") []
- ]
- ]
- ]
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col" ] [
- fieldset [ _class "container"; _id "podcastFields"; if not model.IsPodcast then _disabled ] [
- legend [] [ raw "Podcast Settings" ]
- div [ _class "row" ] [
- div [ _class "col-12 col-md-5 col-lg-4 offset-lg-1 pb-3" ] [
- textField [ _required ] (nameof model.Title) "Title" model.Title []
- ]
- div [ _class "col-12 col-md-4 col-lg-4 pb-3" ] [
- textField [] (nameof model.Subtitle) "Podcast Subtitle" model.Subtitle []
- ]
- div [ _class "col-12 col-md-3 col-lg-2 pb-3" ] [
- numberField [ _required ] (nameof model.ItemsInFeed) "# Episodes" model.ItemsInFeed []
- ]
- ]
- div [ _class "row" ] [
- div [ _class "col-12 col-md-5 col-lg-4 offset-lg-1 pb-3" ] [
- textField [ _required ] (nameof model.AppleCategory) "iTunes Category"
- model.AppleCategory [
- span [ _class "form-text fst-italic" ] [
- a [ _href "https://www.thepodcasthost.com/planning/itunes-podcast-categories/"
- _target "_blank"; _rel "noopener" ] [
- raw "iTunes Category / Subcategory List"
- ]
- ]
- ]
- ]
- div [ _class "col-12 col-md-4 pb-3" ] [
- textField [] (nameof model.AppleSubcategory) "iTunes Subcategory" model.AppleSubcategory
- []
- ]
- div [ _class "col-12 col-md-3 col-lg-2 pb-3" ] [
- selectField [ _required ] (nameof model.Explicit) "Explicit Rating" model.Explicit
- ratings (_.Name) (_.Value) []
- ]
- ]
- div [ _class "row" ] [
- div [ _class "col-12 col-md-6 col-lg-4 offset-xxl-1 pb-3" ] [
- textField [ _required ] (nameof model.DisplayedAuthor) "Displayed Author"
- model.DisplayedAuthor []
- ]
- div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
- emailField [ _required ] (nameof model.Email) "Author E-mail" model.Email [
- span [ _class "form-text fst-italic" ] [
- raw "For iTunes, must match registered e-mail"
- ]
- ]
- ]
- div [ _class "col-12 col-sm-5 col-md-4 col-lg-4 col-xl-3 offset-xl-1 col-xxl-2 offset-xxl-0 pb-3" ] [
- textField [] (nameof model.DefaultMediaType) "Default Media Type"
- model.DefaultMediaType [
- span [ _class "form-text fst-italic" ] [ raw "Optional; blank for no default" ]
- ]
- ]
- div [ _class "col-12 col-sm-7 col-md-8 col-lg-10 offset-lg-1 pb-3" ] [
- textField [ _required ] (nameof model.ImageUrl) "Image URL" model.ImageUrl [
- span [ _class "form-text fst-italic"] [
- raw "Relative URL will be appended to "; txt app.WebLog.UrlBase; raw "/"
- ]
- ]
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col-12 col-lg-10 offset-lg-1" ] [
- textField [ _required ] (nameof model.Summary) "Summary" model.Summary [
- span [ _class "form-text fst-italic" ] [ raw "Displayed in podcast directories" ]
- ]
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col-12 col-lg-10 offset-lg-1" ] [
- textField [] (nameof model.MediaBaseUrl) "Media Base URL" model.MediaBaseUrl [
- span [ _class "form-text fst-italic" ] [
- raw "Optional; prepended to episode media file if present"
- ]
- ]
- ]
- ]
- div [ _class "row" ] [
- div [ _class "col-12 col-lg-5 offset-lg-1 pb-3" ] [
- textField [] (nameof model.FundingUrl) "Funding URL" model.FundingUrl [
- span [ _class "form-text fst-italic" ] [
- raw "Optional; URL describing donation options for this podcast, "
- raw "relative URL supported"
- ]
- ]
- ]
- div [ _class "col-12 col-lg-5 pb-3" ] [
- textField [ _maxlength "128" ] (nameof model.FundingText) "Funding Text"
- model.FundingText [
- span [ _class "form-text fst-italic" ] [ raw "Optional; text for the funding link" ]
- ]
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col-8 col-lg-5 offset-lg-1 pb-3" ] [
- textField [] (nameof model.PodcastGuid) "Podcast GUID" model.PodcastGuid [
- span [ _class "form-text fst-italic" ] [
- raw "Optional; v5 UUID uniquely identifying this podcast; "
- raw "once entered, do not change this value ("
- a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid"
- _target "_blank"; _rel "noopener" ] [
- raw "documentation"
- ]; raw ")"
- ]
- ]
- ]
- div [ _class "col-4 col-lg-3 offset-lg-2 pb-3" ] [
- selectField [] (nameof model.Medium) "Medium" model.Medium mediums (_.Name) (_.Value) [
- span [ _class "form-text fst-italic" ] [
- raw "Optional; medium of the podcast content ("
- a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#medium"
- _target "_blank"; _rel "noopener" ] [
- raw "documentation"
- ]; raw ")"
- ]
- ]
- ]
- ]
- ]
- ]
- ]
- div [ _class "row pb-3" ] [ div [ _class "col text-center" ] [ saveButton ] ]
+ fieldset [ _class "container mb-3 pb-0" ] [
+ legend [] [ raw "Themes" ]
+ span [ _hxGet (relUrl app "admin/theme/list"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml ] []
]
- ]
-]
-
-
-/// Redirect Rule edit form
-let redirectEdit (model: EditRedirectRuleModel) app = [
- let url = relUrl app $"admin/settings/redirect-rules/{model.RuleId}"
- h3 [] [ raw (if model.RuleId < 0 then "Add" else "Edit"); raw " Redirect Rule" ]
- form [ _action url; _hxPost url; _hxTarget "body"; _method "post"; _class "container" ] [
- antiCsrf app
- input [ _type "hidden"; _name "RuleId"; _value (string model.RuleId) ]
- div [ _class "row" ] [
- div [ _class "col-12 col-lg-5 mb-3" ] [
- textField [ _autofocus; _required ] (nameof model.From) "From" model.From [
- span [ _class "form-text" ] [ raw "From local URL/pattern" ]
- ]
- ]
- div [ _class "col-12 col-lg-5 mb-3" ] [
- textField [ _required ] (nameof model.To) "To" model.To [
- span [ _class "form-text" ] [ raw "To URL/pattern" ]
- ]
- ]
- div [ _class "col-12 col-lg-2 mb-3" ] [
- checkboxSwitch [] (nameof model.IsRegex) "Use RegEx" model.IsRegex []
- ]
- ]
- if model.RuleId < 0 then
- div [ _class "row mb-3" ] [
- div [ _class "col-12 text-center" ] [
- label [ _class "me-1" ] [ raw "Add Rule" ]
- div [ _class "btn-group btn-group-sm"; _roleGroup; _ariaLabel "New rule placement button group" ] [
- input [ _type "radio"; _name "InsertAtTop"; _id "at_top"; _class "btn-check"; _value "true" ]
- label [ _class "btn btn-sm btn-outline-secondary"; _for "at_top" ] [ raw "Top" ]
- input [ _type "radio"; _name "InsertAtTop"; _id "at_bot"; _class "btn-check"; _value "false"
- _checked ]
- label [ _class "btn btn-sm btn-outline-secondary"; _for "at_bot" ] [ raw "Bottom" ]
- ]
- ]
- ]
- div [ _class "row mb-3" ] [
- div [ _class "col text-center" ] [
- saveButton; raw " "
- a [ _href (relUrl app "admin/settings/redirect-rules"); _class "btn btn-sm btn-secondary ms-3" ] [
- raw "Cancel"
- ]
- ]
- ]
- ]
-]
-
-
-/// The list of current redirect rules
-let redirectList (model: RedirectRule list) app = [
- // Generate the detail for a redirect rule
- let ruleDetail idx (rule: RedirectRule) =
- let ruleId = $"rule_{idx}"
- div [ _class "row mwl-table-detail"; _id ruleId ] [
- div [ _class "col-5 no-wrap" ] [
- txt rule.From; br []
- small [] [
- let ruleUrl = relUrl app $"admin/settings/redirect-rules/{idx}"
- a [ _href ruleUrl; _hxTarget $"#{ruleId}"; _hxSwap $"{HxSwap.InnerHtml} show:#{ruleId}:top" ] [
- raw "Edit"
- ]
- if idx > 0 then
- span [ _class "text-muted" ] [ raw " • " ]
- a [ _href $"{ruleUrl}/up"; _hxPost $"{ruleUrl}/up" ] [ raw "Move Up" ]
- if idx <> model.Length - 1 then
- span [ _class "text-muted" ] [ raw " • " ]
- a [ _href $"{ruleUrl}/down"; _hxPost $"{ruleUrl}/down" ] [ raw "Move Down" ]
- span [ _class "text-muted" ] [ raw " • " ]
- a [ _class "text-danger"; _href ruleUrl; _hxDelete ruleUrl
- _hxConfirm "Are you sure you want to delete this redirect rule?" ] [
- raw "Delete"
- ]
- ]
- ]
- div [ _class "col-5" ] [ txt rule.To ]
- div [ _class "col-2 text-center" ] [ yesOrNo rule.IsRegex ]
- ]
- h2 [ _class "my-3" ] [ raw app.PageTitle ]
- article [] [
- p [ _class "mb-3" ] [
- a [ _href (relUrl app "admin/settings") ] [ raw "« Back to Settings" ]
- ]
- div [ _class "container" ] [
- div [ _class "row" ] [
- div [ _class "col" ] [
- a [ _href (relUrl app "admin/settings/redirect-rules/-1"); _class "btn btn-primary btn-sm mb-3"
- _hxTarget "#rule_new" ] [
- raw "Add Redirect Rule"
- ]
- ]
+ fieldset [ _class "container mb-3 pb-0" ] [
+ legend [] [ raw "Caches" ]
+ p [ _class "pb-2" ] [
+ raw "myWebLog uses a few caches to ensure that it serves pages as fast as possible. ("
+ a [ _href "https://bitbadger.solutions/open-source/myweblog/advanced.html#cache-management"
+ _target "_blank" ] [
+ raw "more information"
+ ]; raw ")"
]
div [ _class "row" ] [
- div [ _class "col" ] [
- if List.isEmpty model then
- div [ _id "rule_new" ] [
- p [ _class "text-muted text-center fst-italic" ] [
- raw "This web log has no redirect rules defined"
+ div [ _class "col-12 col-lg-6 pb-3" ] [
+ div [ _class "card" ] [
+ header [ _class "card-header text-white bg-secondary" ] [ raw "Web Logs" ]
+ div [ _class "card-body pb-0" ] [
+ h6 [ _class "card-subtitle text-muted pb-3" ] [
+ raw "These caches include the page list and categories for each web log"
+ ]
+ let webLogUrl = $"{cacheBaseUrl}web-log/"
+ form [ _method "post"; _class "container g-0"; _hxNoBoost; _hxTarget "body"
+ _hxSwap $"{HxSwap.InnerHtml} show:window:top" ] [
+ antiCsrf app
+ button [ _type "submit"; _class "btn btn-sm btn-primary mb-2"
+ _hxPost $"{webLogUrl}all/refresh" ] [
+ raw "Refresh All"
+ ]
+ div [ _class "row mwl-table-heading" ] [ div [ _class "col" ] [ raw "Web Log" ] ]
+ yield! WebLogCache.all () |> List.sortBy _.Name |> List.map webLogDetail
]
]
- else
- div [ _class "container g-0" ] [
- div [ _class "row mwl-table-heading" ] [
- div [ _class "col-5" ] [ raw "From" ]
- div [ _class "col-5" ] [ raw "To" ]
- div [ _class "col-2 text-center" ] [ raw "RegEx?" ]
- ]
- ]
- div [ _class "row mwl-table-detail"; _id "rule_new" ] []
- form [ _method "post"; _class "container g-0"; _hxTarget "body" ] [
- antiCsrf app; yield! List.mapi ruleDetail model
- ]
+ ]
]
- ]
- ]
- p [ _class "mt-3 text-muted fst-italic text-center" ] [
- raw "This is an advanced feature; please "
- a [ _href "https://bitbadger.solutions/open-source/myweblog/advanced.html#redirect-rules"
- _target "_blank" ] [
- raw "read and understand the documentation on this feature"
- ]
- raw " before adding rules."
- ]
- ]
-]
-
-
-/// Edit a tag mapping
-let tagMapEdit (model: EditTagMapModel) app = [
- h5 [ _class "my-3" ] [ txt app.PageTitle ]
- form [ _hxPost (relUrl app "admin/settings/tag-mapping/save"); _method "post"; _class "container"
- _hxTarget "#tagList"; _hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
- antiCsrf app
- input [ _type "hidden"; _name "Id"; _value model.Id ]
- div [ _class "row mb-3" ] [
- div [ _class "col-6 col-lg-4 offset-lg-2" ] [
- textField [ _autofocus; _required ] (nameof model.Tag) "Tag" model.Tag []
- ]
- div [ _class "col-6 col-lg-4" ] [
- textField [ _required ] (nameof model.UrlValue) "URL Value" model.UrlValue []
- ]
- ]
- div [ _class "row mb-3" ] [
- div [ _class "col text-center" ] [
- saveButton; raw " "
- a [ _href (relUrl app "admin/settings/tag-mappings"); _class "btn btn-sm btn-secondary ms-3" ] [
- raw "Cancel"
+ div [ _class "col-12 col-lg-6 pb-3" ] [
+ div [ _class "card" ] [
+ header [ _class "card-header text-white bg-secondary" ] [ raw "Themes" ]
+ div [ _class "card-body pb-0" ] [
+ h6 [ _class "card-subtitle text-muted pb-3" ] [
+ raw "The theme template cache is filled on demand as pages are displayed; "
+ raw "refreshing a theme with no cached templates will still refresh its asset cache"
+ ]
+ form [ _method "post"; _class "container g-0"; _hxNoBoost; _hxTarget "body"
+ _hxSwap $"{HxSwap.InnerHtml} show:window:top" ] [
+ antiCsrf app
+ button [ _type "submit"; _class "btn btn-sm btn-primary mb-2"
+ _hxPost $"{themeCacheUrl}all/refresh" ] [
+ raw "Refresh All"
+ ]
+ div [ _class "row mwl-table-heading" ] [
+ div [ _class "col-8" ] [ raw "Theme" ]; div [ _class "col-4" ] [ raw "Cached" ]
+ ]
+ yield! themes |> List.filter (fun t -> t.Id <> ThemeId "admin") |> List.map themeDetail
+ ]
+ ]
+ ]
]
]
]
]
]
-
-/// Display a list of the web log's current tag mappings
-let tagMapList (model: TagMap list) app =
- let tagMapDetail (map: TagMap) =
- let url = relUrl app $"admin/settings/tag-mapping/{map.Id}"
- div [ _class "row mwl-table-detail"; _id $"tag_{map.Id}" ] [
- div [ _class "col no-wrap" ] [
- txt map.Tag; br []
- small [] [
- a [ _href $"{url}/edit"; _hxTarget $"#tag_{map.Id}"
- _hxSwap $"{HxSwap.InnerHtml} show:#tag_{map.Id}:top" ] [
- raw "Edit"
- ]
- span [ _class "text-muted" ] [ raw " • " ]
- a [ _href url; _hxDelete url; _class "text-danger"
- _hxConfirm $"Are you sure you want to delete the mapping for “{map.Tag}”? This action cannot be undone." ] [
- raw "Delete"
- ]
- ]
- ]
- div [ _class "col" ] [ txt map.UrlValue ]
- ]
- div [ _id "tagList"; _class "container" ] [
- div [ _class "row" ] [
- div [ _class "col" ] [
- if List.isEmpty model then
- div [ _id "tag_new" ] [
- p [ _class "text-muted text-center fst-italic" ] [ raw "This web log has no tag mappings" ]
- ]
- else
- div [ _class "container g-0" ] [
- div [ _class "row mwl-table-heading" ] [
- div [ _class "col" ] [ raw "Tag" ]
- div [ _class "col" ] [ raw "URL Value" ]
- ]
- ]
- form [ _method "post"; _class "container g-0"; _hxTarget "#tagList"; _hxSwap HxSwap.OuterHtml ] [
- antiCsrf app
- div [ _class "row mwl-table-detail"; _id "tag_new" ] []
- yield! List.map tagMapDetail model
- ]
- ]
- ]
- ]
- |> List.singleton
-
-
/// Display a list of themes
let themeList (model: DisplayTheme list) app =
let themeCol = "col-12 col-md-6"
@@ -574,185 +188,3 @@ let themeUpload app =
]
]
|> List.singleton
-
-
-/// Web log settings page
-let webLogSettings
- (model: SettingsModel) (themes: Theme list) (pages: Page list) (uploads: UploadDestination list)
- (rss: EditRssModel) (feeds: DisplayCustomFeed list) app = [
- h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " Settings" ]
- article [] [
- p [ _class "text-muted" ] [
- raw "Go to: "; a [ _href "#users" ] [ raw "Users" ]; raw " • "
- a [ _href "#rss-settings" ] [ raw "RSS Settings" ]; raw " • "
- a [ _href "#tag-mappings" ] [ raw "Tag Mappings" ]; raw " • "
- a [ _href (relUrl app "admin/settings/redirect-rules") ] [ raw "Redirect Rules" ]
- ]
- fieldset [ _class "container mb-3" ] [
- legend [] [ raw "Web Log Settings" ]
- form [ _action (relUrl app "admin/settings"); _method "post" ] [
- antiCsrf app
- div [ _class "container g-0" ] [
- div [ _class "row" ] [
- div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
- textField [ _required; _autofocus ] (nameof model.Name) "Name" model.Name []
- ]
- div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
- textField [ _required ] (nameof model.Slug) "Slug" model.Slug [
- span [ _class "form-text" ] [
- span [ _class "badge rounded-pill bg-warning text-dark" ] [ raw "WARNING" ]
- raw " changing this value may break links ("
- a [ _href "https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings"
- _target "_blank" ] [
- raw "more"
- ]; raw ")"
- ]
- ]
- ]
- div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
- textField [] (nameof model.Subtitle) "Subtitle" model.Subtitle []
- ]
- div [ _class "col-12 col-md-6 col-xl-4 offset-xl-1 pb-3" ] [
- selectField [ _required ] (nameof model.ThemeId) "Theme" model.ThemeId themes
- (fun t -> string t.Id) (fun t -> $"{t.Name} (v{t.Version})") []
- ]
- div [ _class "col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3" ] [
- selectField [ _required ] (nameof model.DefaultPage) "Default Page" model.DefaultPage pages
- (fun p -> string p.Id) (_.Title) []
- ]
- div [ _class "col-12 col-md-4 col-xl-2 pb-3" ] [
- numberField [ _required; _min "0"; _max "50" ] (nameof model.PostsPerPage) "Posts per Page"
- model.PostsPerPage []
- ]
- ]
- div [ _class "row" ] [
- div [ _class "col-12 col-md-4 col-xl-3 offset-xl-2 pb-3" ] [
- textField [ _required ] (nameof model.TimeZone) "Time Zone" model.TimeZone []
- ]
- div [ _class "col-12 col-md-4 col-xl-2" ] [
- checkboxSwitch [] (nameof model.AutoHtmx) "Auto-Load htmx" model.AutoHtmx []
- span [ _class "form-text fst-italic" ] [
- a [ _href "https://htmx.org"; _target "_blank"; _rel "noopener" ] [
- raw "What is this?"
- ]
- ]
- ]
- div [ _class "col-12 col-md-4 col-xl-3 pb-3" ] [
- selectField [] (nameof model.Uploads) "Default Upload Destination" model.Uploads uploads
- string string []
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col text-center" ] [
- button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
- ]
- ]
- ]
- ]
- ]
- fieldset [ _id "users"; _class "container mb-3 pb-0" ] [
- legend [] [ raw "Users" ]
- span [ _hxGet (relUrl app "admin/settings/users"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml ] []
- ]
- fieldset [ _id "rss-settings"; _class "container mb-3 pb-0" ] [
- legend [] [ raw "RSS Settings" ]
- form [ _action (relUrl app "admin/settings/rss"); _method "post"; _class "container g-0" ] [
- antiCsrf app
- div [ _class "row pb-3" ] [
- div [ _class "col col-xl-8 offset-xl-2" ] [
- fieldset [ _class "d-flex justify-content-evenly flex-row" ] [
- legend [] [ raw "Feeds Enabled" ]
- checkboxSwitch [] (nameof rss.IsFeedEnabled) "All Posts" rss.IsFeedEnabled []
- checkboxSwitch [] (nameof rss.IsCategoryEnabled) "Posts by Category" rss.IsCategoryEnabled
- []
- checkboxSwitch [] (nameof rss.IsTagEnabled) "Posts by Tag" rss.IsTagEnabled []
- ]
- ]
- ]
- div [ _class "row" ] [
- div [ _class "col-12 col-sm-6 col-md-3 col-xl-2 offset-xl-2 pb-3" ] [
- textField [] (nameof rss.FeedName) "Feed File Name" rss.FeedName [
- span [ _class "form-text" ] [ raw "Default is "; code [] [ raw "feed.xml" ] ]
- ]
- ]
- div [ _class "col-12 col-sm-6 col-md-4 col-xl-2 pb-3" ] [
- numberField [ _required; _min "0" ] (nameof rss.ItemsInFeed) "Items in Feed" rss.ItemsInFeed [
- span [ _class "form-text" ] [
- raw "Set to “0” to use “Posts per Page” setting ("
- raw (string app.WebLog.PostsPerPage); raw ")"
- ]
- ]
- ]
- div [ _class "col-12 col-md-5 col-xl-4 pb-3" ] [
- textField [] (nameof rss.Copyright) "Copyright String" rss.Copyright [
- span [ _class "form-text" ] [
- raw "Can be a "
- a [ _href "https://creativecommons.org/share-your-work/"; _target "_blank"
- _rel "noopener" ] [
- raw "Creative Commons license string"
- ]
- ]
- ]
- ]
- ]
- div [ _class "row pb-3" ] [
- div [ _class "col text-center" ] [
- button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
- ]
- ]
- ]
- fieldset [ _class "container mb-3 pb-0" ] [
- legend [] [ raw "Custom Feeds" ]
- a [ _class "btn btn-sm btn-secondary"; _href (relUrl app "admin/settings/rss/new/edit") ] [
- raw "Add a New Custom Feed"
- ]
- if feeds.Length = 0 then
- p [ _class "text-muted fst-italic text-center" ] [ raw "No custom feeds defined" ]
- else
- form [ _method "post"; _class "container g-0"; _hxTarget "body" ] [
- antiCsrf app
- div [ _class "row mwl-table-heading" ] [
- div [ _class "col-12 col-md-6" ] [
- span [ _class "d-md-none" ] [ raw "Feed" ]
- span [ _class "d-none d-md-inline" ] [ raw "Source" ]
- ]
- div [ _class $"col-12 col-md-6 d-none d-md-inline-block" ] [ raw "Relative Path" ]
- ]
- for feed in feeds do
- div [ _class "row mwl-table-detail" ] [
- div [ _class "col-12 col-md-6" ] [
- txt feed.Source
- if feed.IsPodcast then
- raw " "; span [ _class "badge bg-primary" ] [ raw "PODCAST" ]
- br []
- small [] [
- let feedUrl = relUrl app $"admin/settings/rss/{feed.Id}"
- a [ _href (relUrl app feed.Path); _target "_blank" ] [ raw "View Feed" ]
- span [ _class "text-muted" ] [ raw " • " ]
- a [ _href $"{feedUrl}/edit" ] [ raw "Edit" ]
- span [ _class "text-muted" ] [ raw " • " ]
- a [ _href feedUrl; _hxDelete feedUrl; _class "text-danger"
- _hxConfirm $"Are you sure you want to delete the custom RSS feed based on {feed.Source}? This action cannot be undone." ] [
- raw "Delete"
- ]
- ]
- ]
- div [ _class "col-12 col-md-6" ] [
- small [ _class "d-md-none" ] [ raw "Served at "; txt feed.Path ]
- span [ _class "d-none d-md-inline" ] [ txt feed.Path ]
- ]
- ]
- ]
- ]
- ]
- fieldset [ _id "tag-mappings"; _class "container mb-3 pb-0" ] [
- legend [] [ raw "Tag Mappings" ]
- a [ _href (relUrl app "admin/settings/tag-mapping/new/edit"); _class "btn btn-primary btn-sm mb-3"
- _hxTarget "#tag_new" ] [
- raw "Add a New Tag Mapping"
- ]
- span [ _hxGet (relUrl app "admin/settings/tag-mappings"); _hxTrigger HxTrigger.Load
- _hxSwap HxSwap.OuterHtml ] []
- ]
- ]
-]
diff --git a/src/MyWebLog/Views/Helpers.fs b/src/MyWebLog/Views/Helpers.fs
index 5372127..a087d2c 100644
--- a/src/MyWebLog/Views/Helpers.fs
+++ b/src/MyWebLog/Views/Helpers.fs
@@ -74,6 +74,9 @@ let txt = encodedText
/// Shorthand for raw text in a template
let raw = rawText
+/// Rel attribute to prevent opener information from being provided to the new window
+let _relNoOpener = _rel "noopener"
+
/// The pattern for a long date
let longDatePattern =
ZonedDateTimePattern.CreateWithInvariantCulture("MMMM d, yyyy", DateTimeZoneProviders.Tzdb)
@@ -136,7 +139,7 @@ let passwordField attrs name labelText value extra =
/// Create a select (dropdown) field
let selectField<'T, 'a>
- attrs name labelText value (values: 'T list) (idFunc: 'T -> 'a) (displayFunc: 'T -> string) extra =
+ attrs name labelText value (values: 'T seq) (idFunc: 'T -> 'a) (displayFunc: 'T -> string) extra =
div [ _class "form-floating" ] [
select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [
for item in values do
@@ -161,6 +164,10 @@ let checkboxSwitch attrs name labelText (value: bool) extra =
let saveButton =
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]
+/// A spacer bullet to use between action links
+let actionSpacer =
+ span [ _class "text-muted" ] [ raw " • " ]
+
/// Functions for generating content in varying layouts
module Layout =
diff --git a/src/MyWebLog/Views/Post.fs b/src/MyWebLog/Views/Post.fs
index fa0de21..d7b9499 100644
--- a/src/MyWebLog/Views/Post.fs
+++ b/src/MyWebLog/Views/Post.fs
@@ -66,7 +66,7 @@ let chapterEdit (model: EditChapterModel) app = [
textField (_required :: attrs) (nameof model.LocationGeo) "Geo URL" model.LocationGeo [
em [ _class "form-text" ] [
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended"
- _target "_blank"; _rel "noopener" ] [
+ _target "_blank"; _relNoOpener ] [
raw "see spec"
]
]
@@ -76,10 +76,10 @@ let chapterEdit (model: EditChapterModel) app = [
textField attrs (nameof model.LocationOsm) "OpenStreetMap ID" model.LocationOsm [
em [ _class "form-text" ] [
raw "Optional; "
- a [ _href "https://www.openstreetmap.org/"; _target "_blank"; _rel "noopener" ] [ raw "get ID" ]
+ a [ _href "https://www.openstreetmap.org/"; _target "_blank"; _relNoOpener ] [ raw "get ID" ]
raw ", "
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#osm-recommended"
- _target "_blank"; _rel "noopener" ] [
+ _target "_blank"; _relNoOpener ] [
raw "see spec"
]
]
diff --git a/src/MyWebLog/Views/WebLog.fs b/src/MyWebLog/Views/WebLog.fs
new file mode 100644
index 0000000..2aeff71
--- /dev/null
+++ b/src/MyWebLog/Views/WebLog.fs
@@ -0,0 +1,875 @@
+module MyWebLog.Views.WebLog
+
+open Giraffe.Htmx.Common
+open Giraffe.ViewEngine
+open Giraffe.ViewEngine.Accessibility
+open Giraffe.ViewEngine.Htmx
+open MyWebLog
+open MyWebLog.ViewModels
+
+/// Form to add or edit a category
+let categoryEdit (model: EditCategoryModel) app =
+ div [ _class "col-12" ] [
+ h5 [ _class "my-3" ] [ raw app.PageTitle ]
+ form [ _action (relUrl app "admin/category/save"); _method "post"; _class "container" ] [
+ antiCsrf app
+ input [ _type "hidden"; _name (nameof model.CategoryId); _value model.CategoryId ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-sm-6 col-lg-4 col-xxl-3 offset-xxl-1 mb-3" ] [
+ textField [ _required; _autofocus ] (nameof model.Name) "Name" model.Name []
+ ]
+ div [ _class "col-12 col-sm-6 col-lg-4 col-xxl-3 mb-3" ] [
+ textField [ _required ] (nameof model.Slug) "Slug" model.Slug []
+ ]
+ div [ _class "col-12 col-lg-4 col-xxl-3 offset-xxl-1 mb-3" ] [
+ let cats =
+ app.Categories
+ |> Seq.ofArray
+ |> Seq.filter (fun c -> c.Id <> model.CategoryId)
+ |> Seq.map (fun c ->
+ let parents =
+ c.ParentNames
+ |> Array.map (fun it -> $"{it} » ")
+ |> String.concat ""
+ { Name = c.Id; Value = $"{parents}{c.Name}" })
+ |> Seq.append [ { Name = ""; Value = "– None –" } ]
+ selectField [] (nameof model.ParentId) "Parent Category" model.ParentId cats (_.Name) (_.Value) []
+ ]
+ div [ _class "col-12 col-xl-10 offset-xl-1 mb-3" ] [
+ textField [] (nameof model.Description) "Description" model.Description []
+ ]
+ ]
+ div [ _class "row mb-3" ] [
+ div [ _class "col text-center" ] [
+ saveButton
+ a [ _href (relUrl app "admin/categories"); _class "btn btn-sm btn-secondary ms-3" ] [ raw "Cancel" ]
+ ]
+ ]
+ ]
+ ]
+ |> List.singleton
+
+
+/// Category list page
+let categoryList app = [
+ let catCol = "col-12 col-md-6 col-xl-5 col-xxl-4"
+ let descCol = "col-12 col-md-6 col-xl-7 col-xxl-8"
+ let categoryDetail (cat: DisplayCategory) =
+ div [ _class "row mwl-table-detail"; _id $"cat_{cat.Id}" ] [
+ div [ _class $"{catCol} no-wrap" ] [
+ if cat.ParentNames.Length > 0 then
+ cat.ParentNames
+ |> Seq.ofArray
+ |> Seq.map (fun it -> raw $"{it} 〉 ")
+ |> List.ofSeq
+ |> small [ _class "text-muted" ]
+ raw cat.Name; br []
+ small [] [
+ let catUrl = relUrl app $"admin/category/{cat.Id}"
+ if cat.PostCount > 0 then
+ a [ _href (relUrl app $"category/{cat.Slug}"); _target "_blank" ] [
+ raw $"View { cat.PostCount} Post"; if cat.PostCount <> 1 then raw "s"
+ ]; actionSpacer
+ a [ _href $"{catUrl}/edit"; _hxTarget $"#cat_{cat.Id}"
+ _hxSwap $"{HxSwap.InnerHtml} show:#cat_{cat.Id}:top" ] [
+ raw "Edit"
+ ]; actionSpacer
+ a [ _href catUrl; _hxDelete catUrl; _class "text-danger"
+ _hxConfirm $"Are you sure you want to delete the category “{cat.Name}”? This action cannot be undone." ] [
+ raw "Delete"
+ ]
+ ]
+ ]
+ div [ _class descCol ] [
+ match cat.Description with Some value -> raw value | None -> em [ _class "text-muted" ] [ raw "none" ]
+ ]
+ ]
+
+ h2 [ _class "my-3" ] [ raw app.PageTitle ]
+ article [] [
+ a [ _href (relUrl app "admin/category/new/edit"); _class "btn btn-primary btn-sm mb-3"; _hxTarget "#cat_new" ] [
+ raw "Add a New Category"
+ ]
+ div [ _id "catList"; _class "container" ] [
+ if app.Categories.Length = 0 then
+ div [ _id "cat_new" ] [
+ p [ _class "text-muted fst-italic text-center" ] [
+ raw "This web log has no categories defined"
+ ]
+ ]
+ else
+ div [ _class "container" ] [
+ div [ _class "row mwl-table-heading" ] [
+ div [ _class catCol ] [ raw "Category"; span [ _class "d-md-none" ] [ raw "; Description" ] ]
+ div [ _class $"{descCol} d-none d-md-inline-block" ] [ raw "Description" ]
+ ]
+ ]
+ form [ _method "post"; _class "container" ] [
+ // don't think we need this...
+ // _hxTarget "#catList"; _hxSwap $"{HxSwap.OuterHtml} show:window:top"
+ antiCsrf app
+ div [ _class "row mwl-table-detail"; _id "cat_new" ] []
+ yield! app.Categories |> Seq.ofArray |> Seq.map categoryDetail |> List.ofSeq
+ ]
+ ]
+ ]
+]
+
+
+/// The main dashboard
+let dashboard (model: DashboardModel) app = [
+ h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " • Dashboard" ]
+ article [ _class "container" ] [
+ div [ _class "row" ] [
+ section [ _class "col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3" ] [
+ div [ _class "card" ] [
+ header [ _class "card-header text-white bg-primary" ] [ raw "Posts" ]
+ div [ _class "card-body" ] [
+ h6 [ _class "card-subtitle text-muted pb-3" ] [
+ raw "Published "
+ span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Posts) ]
+ raw " Drafts "
+ span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Drafts) ]
+ ]
+ if app.IsAuthor then
+ a [ _href (relUrl app "admin/posts"); _class "btn btn-secondary me-2" ] [ raw "View All" ]
+ a [ _href (relUrl app "admin/post/new/edit"); _class "btn btn-primary" ] [
+ raw "Write a New Post"
+ ]
+ ]
+ ]
+ ]
+ section [ _class "col-lg-5 col-xl-4 pb-3" ] [
+ div [ _class "card" ] [
+ header [ _class "card-header text-white bg-primary" ] [ raw "Pages" ]
+ div [ _class "card-body" ] [
+ h6 [ _class "card-subtitle text-muted pb-3" ] [
+ raw "All "
+ span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Pages) ]
+ raw " Shown in Page List "
+ span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.ListedPages) ]
+ ]
+ if app.IsAuthor then
+ a [ _href (relUrl app "admin/pages"); _class "btn btn-secondary me-2" ] [ raw "View All" ]
+ a [ _href (relUrl app "admin/page/new/edit"); _class "btn btn-primary" ] [
+ raw "Create a New Page"
+ ]
+ ]
+ ]
+ ]
+ ]
+ div [ _class "row" ] [
+ section [ _class "col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3" ] [
+ div [ _class "card" ] [
+ header [ _class "card-header text-white bg-secondary" ] [ raw "Categories" ]
+ div [ _class "card-body" ] [
+ h6 [ _class "card-subtitle text-muted pb-3"] [
+ raw "All "
+ span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Categories) ]
+ raw " Top Level "
+ span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.TopLevelCategories) ]
+ ]
+ if app.IsWebLogAdmin then
+ a [ _href (relUrl app "admin/categories"); _class "btn btn-secondary me-2" ] [
+ raw "View All"
+ ]
+ a [ _href (relUrl app "admin/category/new/edit"); _class "btn btn-secondary" ] [
+ raw "Add a New Category"
+ ]
+ ]
+ ]
+ ]
+ ]
+ if app.IsWebLogAdmin then
+ div [ _class "row pb-3" ] [
+ div [ _class "col text-end" ] [
+ a [ _href (relUrl app "admin/settings"); _class "btn btn-secondary" ] [ raw "Modify Settings" ]
+ ]
+ ]
+ ]
+]
+
+
+/// Custom RSS feed edit form
+let feedEdit (model: EditCustomFeedModel) (ratings: MetaItem list) (mediums: MetaItem list) app = [
+ h2 [ _class "my-3" ] [ raw app.PageTitle ]
+ article [] [
+ form [ _action (relUrl app "admin/settings/rss/save"); _method "post"; _class "container" ] [
+ antiCsrf app
+ input [ _type "hidden"; _name "Id"; _value model.Id ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col" ] [
+ a [ _href (relUrl app "admin/settings#rss-settings") ] [ raw "« Back to Settings" ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col-12 col-lg-6" ] [
+ fieldset [ _class "container pb-0" ] [
+ legend [] [ raw "Identification" ]
+ div [ _class "row" ] [
+ div [ _class "col" ] [
+ textField [ _required ] (nameof model.Path) "Relative Feed Path" model.Path [
+ span [ _class "form-text fst-italic" ] [ raw "Appended to "; txt app.WebLog.UrlBase ]
+ ]
+ ]
+ ]
+ div [ _class "row" ] [
+ div [ _class "col py-3 d-flex align-self-center justify-content-center" ] [
+ checkboxSwitch [ _onclick "Admin.checkPodcast()"; if model.IsPodcast then _checked ]
+ (nameof model.IsPodcast) "This Is a Podcast Feed" model.IsPodcast []
+ ]
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-lg-6" ] [
+ fieldset [ _class "container pb-0" ] [
+ legend [] [ raw "Feed Source" ]
+ div [ _class "row d-flex align-items-center" ] [
+ div [ _class "col-1 d-flex justify-content-end pb-3" ] [
+ div [ _class "form-check form-check-inline me-0" ] [
+ input [ _type "radio"; _name (nameof model.SourceType); _id "SourceTypeCat"
+ _class "form-check-input"; _value "category"
+ if model.SourceType <> "tag" then _checked
+ _onclick "Admin.customFeedBy('category')" ]
+ label [ _for "SourceTypeCat"; _class "form-check-label d-none" ] [ raw "Category" ]
+ ]
+ ]
+ div [ _class "col-11 pb-3" ] [
+ let cats =
+ app.Categories
+ |> Seq.ofArray
+ |> Seq.map (fun c ->
+ let parents =
+ c.ParentNames
+ |> Array.map (fun it -> $"{it} 〉 ")
+ |> String.concat ""
+ { Name = c.Id; Value = $"{parents}{c.Name}" })
+ |> Seq.append [ { Name = ""; Value = "– Select Category –" } ]
+ selectField [ _id "SourceValueCat"; _required
+ if model.SourceType = "tag" then _disabled ]
+ (nameof model.SourceValue) "Category" model.SourceValue cats (_.Name)
+ (_.Value) []
+ ]
+ div [ _class "col-1 d-flex justify-content-end pb-3" ] [
+ div [ _class "form-check form-check-inline me-0" ] [
+ input [ _type "radio"; _name (nameof model.SourceType); _id "SourceTypeTag"
+ _class "form-check-input"; _value "tag"
+ if model.SourceType= "tag" then _checked
+ _onclick "Admin.customFeedBy('tag')" ]
+ label [ _for "sourceTypeTag"; _class "form-check-label d-none" ] [ raw "Tag" ]
+ ]
+ ]
+ div [ _class "col-11 pb-3" ] [
+ textField [ _id "SourceValueTag"; _required
+ if model.SourceType <> "tag" then _disabled ]
+ (nameof model.SourceValue) "Tag"
+ (if model.SourceType = "tag" then model.SourceValue else "") []
+ ]
+ ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col" ] [
+ fieldset [ _class "container"; _id "podcastFields"; if not model.IsPodcast then _disabled ] [
+ legend [] [ raw "Podcast Settings" ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-md-5 col-lg-4 offset-lg-1 pb-3" ] [
+ textField [ _required ] (nameof model.Title) "Title" model.Title []
+ ]
+ div [ _class "col-12 col-md-4 col-lg-4 pb-3" ] [
+ textField [] (nameof model.Subtitle) "Podcast Subtitle" model.Subtitle []
+ ]
+ div [ _class "col-12 col-md-3 col-lg-2 pb-3" ] [
+ numberField [ _required ] (nameof model.ItemsInFeed) "# Episodes" model.ItemsInFeed []
+ ]
+ ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-md-5 col-lg-4 offset-lg-1 pb-3" ] [
+ textField [ _required ] (nameof model.AppleCategory) "iTunes Category"
+ model.AppleCategory [
+ span [ _class "form-text fst-italic" ] [
+ a [ _href "https://www.thepodcasthost.com/planning/itunes-podcast-categories/"
+ _target "_blank"; _relNoOpener ] [
+ raw "iTunes Category / Subcategory List"
+ ]
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-md-4 pb-3" ] [
+ textField [] (nameof model.AppleSubcategory) "iTunes Subcategory" model.AppleSubcategory
+ []
+ ]
+ div [ _class "col-12 col-md-3 col-lg-2 pb-3" ] [
+ selectField [ _required ] (nameof model.Explicit) "Explicit Rating" model.Explicit
+ ratings (_.Name) (_.Value) []
+ ]
+ ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-md-6 col-lg-4 offset-xxl-1 pb-3" ] [
+ textField [ _required ] (nameof model.DisplayedAuthor) "Displayed Author"
+ model.DisplayedAuthor []
+ ]
+ div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
+ emailField [ _required ] (nameof model.Email) "Author E-mail" model.Email [
+ span [ _class "form-text fst-italic" ] [
+ raw "For iTunes, must match registered e-mail"
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-sm-5 col-md-4 col-lg-4 col-xl-3 offset-xl-1 col-xxl-2 offset-xxl-0 pb-3" ] [
+ textField [] (nameof model.DefaultMediaType) "Default Media Type"
+ model.DefaultMediaType [
+ span [ _class "form-text fst-italic" ] [ raw "Optional; blank for no default" ]
+ ]
+ ]
+ div [ _class "col-12 col-sm-7 col-md-8 col-lg-10 offset-lg-1 pb-3" ] [
+ textField [ _required ] (nameof model.ImageUrl) "Image URL" model.ImageUrl [
+ span [ _class "form-text fst-italic"] [
+ raw "Relative URL will be appended to "; txt app.WebLog.UrlBase; raw "/"
+ ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col-12 col-lg-10 offset-lg-1" ] [
+ textField [ _required ] (nameof model.Summary) "Summary" model.Summary [
+ span [ _class "form-text fst-italic" ] [ raw "Displayed in podcast directories" ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col-12 col-lg-10 offset-lg-1" ] [
+ textField [] (nameof model.MediaBaseUrl) "Media Base URL" model.MediaBaseUrl [
+ span [ _class "form-text fst-italic" ] [
+ raw "Optional; prepended to episode media file if present"
+ ]
+ ]
+ ]
+ ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-lg-5 offset-lg-1 pb-3" ] [
+ textField [] (nameof model.FundingUrl) "Funding URL" model.FundingUrl [
+ span [ _class "form-text fst-italic" ] [
+ raw "Optional; URL describing donation options for this podcast, "
+ raw "relative URL supported"
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-lg-5 pb-3" ] [
+ textField [ _maxlength "128" ] (nameof model.FundingText) "Funding Text"
+ model.FundingText [
+ span [ _class "form-text fst-italic" ] [ raw "Optional; text for the funding link" ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col-8 col-lg-5 offset-lg-1 pb-3" ] [
+ textField [] (nameof model.PodcastGuid) "Podcast GUID" model.PodcastGuid [
+ span [ _class "form-text fst-italic" ] [
+ raw "Optional; v5 UUID uniquely identifying this podcast; "
+ raw "once entered, do not change this value ("
+ a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid"
+ _target "_blank"; _relNoOpener ] [
+ raw "documentation"
+ ]; raw ")"
+ ]
+ ]
+ ]
+ div [ _class "col-4 col-lg-3 offset-lg-2 pb-3" ] [
+ selectField [] (nameof model.Medium) "Medium" model.Medium mediums (_.Name) (_.Value) [
+ span [ _class "form-text fst-italic" ] [
+ raw "Optional; medium of the podcast content ("
+ a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#medium"
+ _target "_blank"; _relNoOpener ] [
+ raw "documentation"
+ ]; raw ")"
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [ div [ _class "col text-center" ] [ saveButton ] ]
+ ]
+ ]
+]
+
+
+/// Redirect Rule edit form
+let redirectEdit (model: EditRedirectRuleModel) app = [
+ let url = relUrl app $"admin/settings/redirect-rules/{model.RuleId}"
+ h3 [] [ raw (if model.RuleId < 0 then "Add" else "Edit"); raw " Redirect Rule" ]
+ form [ _action url; _hxPost url; _hxTarget "body"; _method "post"; _class "container" ] [
+ antiCsrf app
+ input [ _type "hidden"; _name "RuleId"; _value (string model.RuleId) ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-lg-5 mb-3" ] [
+ textField [ _autofocus; _required ] (nameof model.From) "From" model.From [
+ span [ _class "form-text" ] [ raw "From local URL/pattern" ]
+ ]
+ ]
+ div [ _class "col-12 col-lg-5 mb-3" ] [
+ textField [ _required ] (nameof model.To) "To" model.To [
+ span [ _class "form-text" ] [ raw "To URL/pattern" ]
+ ]
+ ]
+ div [ _class "col-12 col-lg-2 mb-3" ] [
+ checkboxSwitch [] (nameof model.IsRegex) "Use RegEx" model.IsRegex []
+ ]
+ ]
+ if model.RuleId < 0 then
+ div [ _class "row mb-3" ] [
+ div [ _class "col-12 text-center" ] [
+ label [ _class "me-1" ] [ raw "Add Rule" ]
+ div [ _class "btn-group btn-group-sm"; _roleGroup; _ariaLabel "New rule placement button group" ] [
+ input [ _type "radio"; _name "InsertAtTop"; _id "at_top"; _class "btn-check"; _value "true" ]
+ label [ _class "btn btn-sm btn-outline-secondary"; _for "at_top" ] [ raw "Top" ]
+ input [ _type "radio"; _name "InsertAtTop"; _id "at_bot"; _class "btn-check"; _value "false"
+ _checked ]
+ label [ _class "btn btn-sm btn-outline-secondary"; _for "at_bot" ] [ raw "Bottom" ]
+ ]
+ ]
+ ]
+ div [ _class "row mb-3" ] [
+ div [ _class "col text-center" ] [
+ saveButton; raw " "
+ a [ _href (relUrl app "admin/settings/redirect-rules"); _class "btn btn-sm btn-secondary ms-3" ] [
+ raw "Cancel"
+ ]
+ ]
+ ]
+ ]
+]
+
+
+/// The list of current redirect rules
+let redirectList (model: RedirectRule list) app = [
+ // Generate the detail for a redirect rule
+ let ruleDetail idx (rule: RedirectRule) =
+ let ruleId = $"rule_{idx}"
+ div [ _class "row mwl-table-detail"; _id ruleId ] [
+ div [ _class "col-5 no-wrap" ] [
+ txt rule.From; br []
+ small [] [
+ let ruleUrl = relUrl app $"admin/settings/redirect-rules/{idx}"
+ a [ _href ruleUrl; _hxTarget $"#{ruleId}"; _hxSwap $"{HxSwap.InnerHtml} show:#{ruleId}:top" ] [
+ raw "Edit"
+ ]
+ if idx > 0 then
+ actionSpacer; a [ _href $"{ruleUrl}/up"; _hxPost $"{ruleUrl}/up" ] [ raw "Move Up" ]
+ if idx <> model.Length - 1 then
+ actionSpacer; a [ _href $"{ruleUrl}/down"; _hxPost $"{ruleUrl}/down" ] [ raw "Move Down" ]
+ actionSpacer
+ a [ _class "text-danger"; _href ruleUrl; _hxDelete ruleUrl
+ _hxConfirm "Are you sure you want to delete this redirect rule?" ] [
+ raw "Delete"
+ ]
+ ]
+ ]
+ div [ _class "col-5" ] [ txt rule.To ]
+ div [ _class "col-2 text-center" ] [ yesOrNo rule.IsRegex ]
+ ]
+ h2 [ _class "my-3" ] [ raw app.PageTitle ]
+ article [] [
+ p [ _class "mb-3" ] [
+ a [ _href (relUrl app "admin/settings") ] [ raw "« Back to Settings" ]
+ ]
+ div [ _class "container" ] [
+ p [] [
+ a [ _href (relUrl app "admin/settings/redirect-rules/-1"); _class "btn btn-primary btn-sm mb-3"
+ _hxTarget "#rule_new" ] [
+ raw "Add Redirect Rule"
+ ]
+ ]
+ if List.isEmpty model then
+ div [ _id "rule_new" ] [
+ p [ _class "text-muted text-center fst-italic" ] [
+ raw "This web log has no redirect rules defined"
+ ]
+ ]
+ else
+ div [ _class "container g-0" ] [
+ div [ _class "row mwl-table-heading" ] [
+ div [ _class "col-5" ] [ raw "From" ]
+ div [ _class "col-5" ] [ raw "To" ]
+ div [ _class "col-2 text-center" ] [ raw "RegEx?" ]
+ ]
+ ]
+ div [ _class "row mwl-table-detail"; _id "rule_new" ] []
+ form [ _method "post"; _class "container g-0"; _hxTarget "body" ] [
+ antiCsrf app; yield! List.mapi ruleDetail model
+ ]
+ ]
+ p [ _class "mt-3 text-muted fst-italic text-center" ] [
+ raw "This is an advanced feature; please "
+ a [ _href "https://bitbadger.solutions/open-source/myweblog/advanced.html#redirect-rules"
+ _target "_blank" ] [
+ raw "read and understand the documentation on this feature"
+ ]
+ raw " before adding rules."
+ ]
+ ]
+]
+
+
+/// Edit a tag mapping
+let tagMapEdit (model: EditTagMapModel) app = [
+ h5 [ _class "my-3" ] [ txt app.PageTitle ]
+ form [ _hxPost (relUrl app "admin/settings/tag-mapping/save"); _method "post"; _class "container"
+ _hxTarget "#tagList"; _hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
+ antiCsrf app
+ input [ _type "hidden"; _name "Id"; _value model.Id ]
+ div [ _class "row mb-3" ] [
+ div [ _class "col-6 col-lg-4 offset-lg-2" ] [
+ textField [ _autofocus; _required ] (nameof model.Tag) "Tag" model.Tag []
+ ]
+ div [ _class "col-6 col-lg-4" ] [
+ textField [ _required ] (nameof model.UrlValue) "URL Value" model.UrlValue []
+ ]
+ ]
+ div [ _class "row mb-3" ] [
+ div [ _class "col text-center" ] [
+ saveButton; raw " "
+ a [ _href (relUrl app "admin/settings/tag-mappings"); _class "btn btn-sm btn-secondary ms-3" ] [
+ raw "Cancel"
+ ]
+ ]
+ ]
+ ]
+]
+
+
+/// Display a list of the web log's current tag mappings
+let tagMapList (model: TagMap list) app =
+ let tagMapDetail (map: TagMap) =
+ let url = relUrl app $"admin/settings/tag-mapping/{map.Id}"
+ div [ _class "row mwl-table-detail"; _id $"tag_{map.Id}" ] [
+ div [ _class "col no-wrap" ] [
+ txt map.Tag; br []
+ small [] [
+ a [ _href $"{url}/edit"; _hxTarget $"#tag_{map.Id}"
+ _hxSwap $"{HxSwap.InnerHtml} show:#tag_{map.Id}:top" ] [
+ raw "Edit"
+ ]; actionSpacer
+ a [ _href url; _hxDelete url; _class "text-danger"
+ _hxConfirm $"Are you sure you want to delete the mapping for “{map.Tag}”? This action cannot be undone." ] [
+ raw "Delete"
+ ]
+ ]
+ ]
+ div [ _class "col" ] [ txt map.UrlValue ]
+ ]
+ div [ _id "tagList"; _class "container" ] [
+ if List.isEmpty model then
+ div [ _id "tag_new" ] [
+ p [ _class "text-muted text-center fst-italic" ] [ raw "This web log has no tag mappings" ]
+ ]
+ else
+ div [ _class "container g-0" ] [
+ div [ _class "row mwl-table-heading" ] [
+ div [ _class "col" ] [ raw "Tag" ]
+ div [ _class "col" ] [ raw "URL Value" ]
+ ]
+ ]
+ form [ _method "post"; _class "container g-0"; _hxTarget "#tagList"; _hxSwap HxSwap.OuterHtml ] [
+ antiCsrf app
+ div [ _class "row mwl-table-detail"; _id "tag_new" ] []
+ yield! List.map tagMapDetail model
+ ]
+ ]
+ |> List.singleton
+
+
+/// The list of uploaded files for a web log
+let uploadList (model: DisplayUpload seq) app = [
+ let webLogBase = $"upload/{app.WebLog.Slug}/"
+ let relativeBase = relUrl app $"upload/{app.WebLog.Slug}/"
+ let absoluteBase = app.WebLog.AbsoluteUrl(Permalink webLogBase)
+ let uploadDetail (upload: DisplayUpload) =
+ div [ _class "row mwl-table-detail" ] [
+ div [ _class "col-6" ] [
+ let badgeClass = if upload.Source = string Disk then "secondary" else "primary"
+ let pathAndName = $"{upload.Path}{upload.Name}"
+ span [ _class $"badge bg-{badgeClass} text-uppercase float-end mt-1" ] [ raw upload.Source ]
+ raw upload.Name; br []
+ small [] [
+ a [ _href $"{relativeBase}{pathAndName}"; _target "_blank" ] [ raw "View File" ]
+ actionSpacer; span [ _class "text-muted" ] [ raw "Copy " ]
+ a [ _href $"{absoluteBase}{pathAndName}"; _hxNoBoost
+ _onclick $"return Admin.copyText('{absoluteBase}{pathAndName}', this)" ] [
+ raw "Absolute"
+ ]
+ span [ _class "text-muted" ] [ raw " | " ]
+ a [ _href $"{relativeBase}{pathAndName}"; _hxNoBoost
+ _onclick $"return Admin.copyText('{relativeBase}{pathAndName}', this)" ] [
+ raw "Relative"
+ ]
+ if app.WebLog.ExtraPath <> "" then
+ span [ _class "text-muted" ] [ raw " | " ]
+ a [ _href $"{webLogBase}{pathAndName}"; _hxNoBoost
+ _onclick $"return Admin.copyText('/{webLogBase}{pathAndName}', this)" ] [
+ raw "For Post"
+ ]
+ span [ _class "text-muted" ] [ raw " Link" ]
+ if app.IsWebLogAdmin then
+ actionSpacer
+ let deleteUrl =
+ if upload.Source = string "Disk" then $"admin/upload/disk/{pathAndName}"
+ else $"admin/upload/{upload.Id}"
+ |> relUrl app
+ a [ _href deleteUrl; _hxDelete deleteUrl; _class "text-danger"
+ _hxConfirm $"Are you sure you want to delete {upload.Name}? This action cannot be undone." ] [
+ raw "Delete"
+ ]
+ ]
+ ]
+ div [ _class "col-3" ] [ raw upload.Path ]
+ div [ _class "col-3" ] [
+ raw (match upload.UpdatedOn with Some updated -> updated.ToString "yyyy-MM-dd/HH:mm" | None -> "--")
+ ]
+ ]
+
+ h2 [ _class "my-3" ] [ raw app.PageTitle ]
+ article [] [
+ a [ _href (relUrl app "admin/upload/new"); _class "btn btn-primary btn-sm mb-3" ] [ raw "Upload a New File" ]
+ form [ _method "post"; _class "container"; _hxTarget "body" ] [
+ antiCsrf app
+ div [ _class "row" ] [
+ div [ _class "col text-center" ] [
+ em [ _class "text-muted" ] [ raw "Uploaded files served from" ]; br []; raw relativeBase
+ ]
+ ]
+ if Seq.isEmpty model then
+ div [ _class "row" ] [
+ div [ _class "col text-muted fst-italic text-center" ] [
+ br []; raw "This web log has uploaded files"
+ ]
+ ]
+ else
+ div [ _class "row mwl-table-heading" ] [
+ div [ _class "col-6" ] [ raw "File Name" ]
+ div [ _class "col-3" ] [ raw "Path" ]
+ div [ _class "col-3" ] [ raw "File Date/Time" ]
+ ]
+ yield! model |> Seq.map uploadDetail
+ ]
+ ]
+]
+
+
+/// Form to upload a new file
+let uploadNew app = [
+ h2 [ _class "my-3" ] [ raw app.PageTitle ]
+ article [] [
+ form [ _action (relUrl app "admin/upload/save"); _method "post"; _class "container"
+ _enctype "multipart/form-data"; _hxNoBoost ] [
+ antiCsrf app
+ div [ _class "row" ] [
+ div [ _class "col-12 col-md-6 pb-3" ] [
+ div [ _class "form-floating" ] [
+ input [ _type "file"; _id "file"; _name "File"; _class "form-control"; _placeholder "File"
+ _required ]
+ label [ _for "file" ] [ raw "File to Upload" ]
+ ]
+ ]
+ div [ _class "col-12 col-md-6 pb-3 d-flex align-self-center justify-content-around" ] [
+ raw "Destination"; br []
+ div [ _class "btn-group"; _roleGroup; _ariaLabel "Upload destination button group" ] [
+ input [ _type "radio"; _name "Destination"; _id "destination_db"; _class "btn-check"
+ _value (string Database); if app.WebLog.Uploads = Database then _checked ]
+ label [ _class "btn btn-outline-primary"; _for "destination_db" ] [ raw (string Database) ]
+ input [ _type "radio"; _name "Destination"; _id "destination_disk"; _class "btn-check"
+ _value (string Disk); if app.WebLog.Uploads= Disk then _checked ]
+ label [ _class "btn btn-outline-secondary"; _for "destination_disk" ] [ raw "Disk" ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col text-center" ] [
+ button [ _type "submit"; _class "btn btn-primary" ] [ raw "Upload File" ]
+ ]
+ ]
+ ]
+ ]
+]
+
+
+/// Web log settings page
+let webLogSettings
+ (model: SettingsModel) (themes: Theme list) (pages: Page list) (uploads: UploadDestination list)
+ (rss: EditRssModel) (feeds: DisplayCustomFeed list) app = [
+ h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " Settings" ]
+ article [] [
+ p [ _class "text-muted" ] [
+ raw "Go to: "; a [ _href "#users" ] [ raw "Users" ]; raw " • "
+ a [ _href "#rss-settings" ] [ raw "RSS Settings" ]; raw " • "
+ a [ _href "#tag-mappings" ] [ raw "Tag Mappings" ]; raw " • "
+ a [ _href (relUrl app "admin/settings/redirect-rules") ] [ raw "Redirect Rules" ]
+ ]
+ fieldset [ _class "container mb-3" ] [
+ legend [] [ raw "Web Log Settings" ]
+ form [ _action (relUrl app "admin/settings"); _method "post" ] [
+ antiCsrf app
+ div [ _class "container g-0" ] [
+ div [ _class "row" ] [
+ div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
+ textField [ _required; _autofocus ] (nameof model.Name) "Name" model.Name []
+ ]
+ div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
+ textField [ _required ] (nameof model.Slug) "Slug" model.Slug [
+ span [ _class "form-text" ] [
+ span [ _class "badge rounded-pill bg-warning text-dark" ] [ raw "WARNING" ]
+ raw " changing this value may break links ("
+ a [ _href "https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings"
+ _target "_blank" ] [
+ raw "more"
+ ]; raw ")"
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
+ textField [] (nameof model.Subtitle) "Subtitle" model.Subtitle []
+ ]
+ div [ _class "col-12 col-md-6 col-xl-4 offset-xl-1 pb-3" ] [
+ selectField [ _required ] (nameof model.ThemeId) "Theme" model.ThemeId themes
+ (fun t -> string t.Id) (fun t -> $"{t.Name} (v{t.Version})") []
+ ]
+ div [ _class "col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3" ] [
+ selectField [ _required ] (nameof model.DefaultPage) "Default Page" model.DefaultPage pages
+ (fun p -> string p.Id) (_.Title) []
+ ]
+ div [ _class "col-12 col-md-4 col-xl-2 pb-3" ] [
+ numberField [ _required; _min "0"; _max "50" ] (nameof model.PostsPerPage) "Posts per Page"
+ model.PostsPerPage []
+ ]
+ ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-md-4 col-xl-3 offset-xl-2 pb-3" ] [
+ textField [ _required ] (nameof model.TimeZone) "Time Zone" model.TimeZone []
+ ]
+ div [ _class "col-12 col-md-4 col-xl-2" ] [
+ checkboxSwitch [] (nameof model.AutoHtmx) "Auto-Load htmx" model.AutoHtmx []
+ span [ _class "form-text fst-italic" ] [
+ a [ _href "https://htmx.org"; _target "_blank"; _relNoOpener ] [ raw "What is this?" ]
+ ]
+ ]
+ div [ _class "col-12 col-md-4 col-xl-3 pb-3" ] [
+ selectField [] (nameof model.Uploads) "Default Upload Destination" model.Uploads uploads
+ string string []
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col text-center" ] [
+ button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ fieldset [ _id "users"; _class "container mb-3 pb-0" ] [
+ legend [] [ raw "Users" ]
+ span [ _hxGet (relUrl app "admin/settings/users"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml ] []
+ ]
+ fieldset [ _id "rss-settings"; _class "container mb-3 pb-0" ] [
+ legend [] [ raw "RSS Settings" ]
+ form [ _action (relUrl app "admin/settings/rss"); _method "post"; _class "container g-0" ] [
+ antiCsrf app
+ div [ _class "row pb-3" ] [
+ div [ _class "col col-xl-8 offset-xl-2" ] [
+ fieldset [ _class "d-flex justify-content-evenly flex-row" ] [
+ legend [] [ raw "Feeds Enabled" ]
+ checkboxSwitch [] (nameof rss.IsFeedEnabled) "All Posts" rss.IsFeedEnabled []
+ checkboxSwitch [] (nameof rss.IsCategoryEnabled) "Posts by Category" rss.IsCategoryEnabled
+ []
+ checkboxSwitch [] (nameof rss.IsTagEnabled) "Posts by Tag" rss.IsTagEnabled []
+ ]
+ ]
+ ]
+ div [ _class "row" ] [
+ div [ _class "col-12 col-sm-6 col-md-3 col-xl-2 offset-xl-2 pb-3" ] [
+ textField [] (nameof rss.FeedName) "Feed File Name" rss.FeedName [
+ span [ _class "form-text" ] [ raw "Default is "; code [] [ raw "feed.xml" ] ]
+ ]
+ ]
+ div [ _class "col-12 col-sm-6 col-md-4 col-xl-2 pb-3" ] [
+ numberField [ _required; _min "0" ] (nameof rss.ItemsInFeed) "Items in Feed" rss.ItemsInFeed [
+ span [ _class "form-text" ] [
+ raw "Set to “0” to use “Posts per Page” setting ("
+ raw (string app.WebLog.PostsPerPage); raw ")"
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-md-5 col-xl-4 pb-3" ] [
+ textField [] (nameof rss.Copyright) "Copyright String" rss.Copyright [
+ span [ _class "form-text" ] [
+ raw "Can be a "
+ a [ _href "https://creativecommons.org/share-your-work/"; _target "_blank"
+ _relNoOpener ] [
+ raw "Creative Commons license string"
+ ]
+ ]
+ ]
+ ]
+ ]
+ div [ _class "row pb-3" ] [
+ div [ _class "col text-center" ] [
+ button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
+ ]
+ ]
+ ]
+ fieldset [ _class "container mb-3 pb-0" ] [
+ legend [] [ raw "Custom Feeds" ]
+ a [ _class "btn btn-sm btn-secondary"; _href (relUrl app "admin/settings/rss/new/edit") ] [
+ raw "Add a New Custom Feed"
+ ]
+ if feeds.Length = 0 then
+ p [ _class "text-muted fst-italic text-center" ] [ raw "No custom feeds defined" ]
+ else
+ form [ _method "post"; _class "container g-0"; _hxTarget "body" ] [
+ antiCsrf app
+ div [ _class "row mwl-table-heading" ] [
+ div [ _class "col-12 col-md-6" ] [
+ span [ _class "d-md-none" ] [ raw "Feed" ]
+ span [ _class "d-none d-md-inline" ] [ raw "Source" ]
+ ]
+ div [ _class $"col-12 col-md-6 d-none d-md-inline-block" ] [ raw "Relative Path" ]
+ ]
+ for feed in feeds do
+ div [ _class "row mwl-table-detail" ] [
+ div [ _class "col-12 col-md-6" ] [
+ txt feed.Source
+ if feed.IsPodcast then
+ raw " "; span [ _class "badge bg-primary" ] [ raw "PODCAST" ]
+ br []
+ small [] [
+ let feedUrl = relUrl app $"admin/settings/rss/{feed.Id}"
+ a [ _href (relUrl app feed.Path); _target "_blank" ] [ raw "View Feed" ]
+ actionSpacer
+ a [ _href $"{feedUrl}/edit" ] [ raw "Edit" ]; actionSpacer
+ a [ _href feedUrl; _hxDelete feedUrl; _class "text-danger"
+ _hxConfirm $"Are you sure you want to delete the custom RSS feed based on {feed.Source}? This action cannot be undone." ] [
+ raw "Delete"
+ ]
+ ]
+ ]
+ div [ _class "col-12 col-md-6" ] [
+ small [ _class "d-md-none" ] [ raw "Served at "; txt feed.Path ]
+ span [ _class "d-none d-md-inline" ] [ txt feed.Path ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ fieldset [ _id "tag-mappings"; _class "container mb-3 pb-0" ] [
+ legend [] [ raw "Tag Mappings" ]
+ a [ _href (relUrl app "admin/settings/tag-mapping/new/edit"); _class "btn btn-primary btn-sm mb-3"
+ _hxTarget "#tag_new" ] [
+ raw "Add a New Tag Mapping"
+ ]
+ span [ _hxGet (relUrl app "admin/settings/tag-mappings"); _hxTrigger HxTrigger.Load
+ _hxSwap HxSwap.OuterHtml ] []
+ ]
+ ]
+]
diff --git a/src/admin-theme/admin-dashboard.liquid b/src/admin-theme/admin-dashboard.liquid
deleted file mode 100644
index 4d8ac30..0000000
--- a/src/admin-theme/admin-dashboard.liquid
+++ /dev/null
@@ -1,92 +0,0 @@
-
{{ page_title }}
-
-
-
-
diff --git a/src/admin-theme/category-edit.liquid b/src/admin-theme/category-edit.liquid
deleted file mode 100644
index 3cb44ef..0000000
--- a/src/admin-theme/category-edit.liquid
+++ /dev/null
@@ -1,52 +0,0 @@
-
diff --git a/src/admin-theme/category-list-body.liquid b/src/admin-theme/category-list-body.liquid
deleted file mode 100644
index 83984a9..0000000
--- a/src/admin-theme/category-list-body.liquid
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
- {%- assign cat_count = categories | size -%}
- {% if cat_count > 0 %}
- {%- assign cat_col = "col-12 col-md-6 col-xl-5 col-xxl-4" -%}
- {%- assign desc_col = "col-12 col-md-6 col-xl-7 col-xxl-8" -%}
-
-
-
Category; Description
-
Description
-
-
-
- {%- else -%}
-
-
This web log has no categories defined
-
- {%- endif %}
-
-
-
diff --git a/src/admin-theme/category-list.liquid b/src/admin-theme/category-list.liquid
deleted file mode 100644
index 689c41c..0000000
--- a/src/admin-theme/category-list.liquid
+++ /dev/null
@@ -1,8 +0,0 @@
-{{ page_title }}
-
-
- Add a New Category
-
- {{ category_list }}
-
diff --git a/src/admin-theme/upload-list.liquid b/src/admin-theme/upload-list.liquid
deleted file mode 100644
index 24c5a9d..0000000
--- a/src/admin-theme/upload-list.liquid
+++ /dev/null
@@ -1,75 +0,0 @@
-{{ page_title }}
-
- {%- capture base_url %}{{ "" | relative_link }}{% endcapture -%}
- {%- capture upload_path %}upload/{{ web_log.slug }}/{% endcapture -%}
- {%- capture upload_base %}{{ base_url }}{{ upload_path }}{% endcapture -%}
- Upload a New File
-
-
diff --git a/src/admin-theme/upload-new.liquid b/src/admin-theme/upload-new.liquid
deleted file mode 100644
index fcd9e8d..0000000
--- a/src/admin-theme/upload-new.liquid
+++ /dev/null
@@ -1,29 +0,0 @@
-{{ page_title }}
-
-
-