Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
15 changed files with 1009 additions and 1054 deletions
Showing only changes of commit 3998399e11 - Show all commits

View File

@ -27,35 +27,13 @@ module Dashboard =
ListedPages = listed ListedPages = listed
Categories = cats Categories = cats
TopLevelCategories = topCats } 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 // GET /admin/administration
let admin : HttpHandler = requireAccess Administrator >=> fun next ctx -> task { let admin : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
let! themes = ctx.Data.Theme.All() let! themes = ctx.Data.Theme.All()
let cachedTemplates = TemplateCache.allNames () return! adminPage "myWebLog Administration" true next ctx (Views.Admin.dashboard themes)
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
} }
/// Redirect the user to the admin dashboard /// Redirect the user to the admin dashboard
@ -117,43 +95,24 @@ module Category =
open MyWebLog.Data open MyWebLog.Data
// GET /admin/categories // GET /admin/categories
let all : HttpHandler = fun next ctx -> task { let all : HttpHandler = fun next ctx ->
match! TemplateCache.get adminTheme "category-list-body" ctx.Data with adminPage "Categories" true next ctx Views.WebLog.categoryList
| 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
// GET /admin/category/{id}/edit // GET /admin/category/{id}/edit
let edit catId : HttpHandler = fun next ctx -> task { let edit catId : HttpHandler = fun next ctx -> task {
let! result = task { let! result = task {
match catId with 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 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 | None -> return None
} }
match result with match result with
| Some (title, cat) -> | Some (title, cat) ->
return! return!
hashForPage title Views.WebLog.categoryEdit (EditCategoryModel.FromCategory cat)
|> withAntiCsrf ctx |> adminBarePage title true next ctx
|> addToHash ViewContext.Model (EditCategoryModel.FromCategory cat)
|> adminBareView "category-edit" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -171,16 +130,16 @@ module Category =
Name = model.Name Name = model.Name
Slug = model.Slug Slug = model.Slug
Description = if model.Description = "" then None else Some model.Description 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! (if model.IsNew then data.Category.Add else data.Category.Update) updatedCat
do! CategoryCache.update ctx do! CategoryCache.update ctx
do! addMessage ctx { UserMessage.Success with Message = "Category saved successfully" } do! addMessage ctx { UserMessage.Success with Message = "Category saved successfully" }
return! bare next ctx return! all next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// POST /admin/category/{id}/delete // DELETE /admin/category/{id}
let delete catId : HttpHandler = fun next ctx -> task { let delete catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! result = ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id let! result = ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id
match result with match result with
| CategoryDeleted | CategoryDeleted
@ -194,7 +153,7 @@ module Category =
do! addMessage ctx { UserMessage.Success with Message = "Category deleted successfully"; Detail = detail } do! addMessage ctx { UserMessage.Success with Message = "Category deleted successfully"; Detail = detail }
| CategoryNotFound -> | CategoryNotFound ->
do! addMessage ctx { UserMessage.Error with Message = "Category not found; cannot delete" } 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 // GET /admin/settings/redirect-rules
let all : HttpHandler = fun next ctx -> 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] // GET /admin/settings/redirect-rules/[index]
let edit idx : HttpHandler = fun next ctx -> let edit idx : HttpHandler = fun next ctx ->
let titleAndView = let titleAndView =
if idx = -1 then 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 else
let rules = ctx.WebLog.RedirectRules let rules = ctx.WebLog.RedirectRules
if rules.Length < idx || idx < 0 then if rules.Length < idx || idx < 0 then
None None
else 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 match titleAndView with
| Some (title, view) -> adminBarePage $"{title} Redirect Rule" true next ctx view | Some (title, view) -> adminBarePage $"{title} Redirect Rule" true next ctx view
| None -> Error.notFound next ctx | None -> Error.notFound next ctx
@ -284,7 +244,7 @@ module TagMapping =
// GET /admin/settings/tag-mappings // GET /admin/settings/tag-mappings
let all : HttpHandler = fun next ctx -> task { let all : HttpHandler = fun next ctx -> task {
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id 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 // GET /admin/settings/tag-mapping/{id}/edit
@ -296,7 +256,7 @@ module TagMapping =
match! tagMap with match! tagMap with
| Some tm -> | Some tm ->
return! 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 |> adminBarePage (if isNew then "Add Tag Mapping" else $"Mapping for {tm.Tag} Tag") true next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -497,7 +457,7 @@ module WebLog =
let uploads = [ Database; Disk ] let uploads = [ Database; Disk ]
let feeds = ctx.WebLog.Rss.CustomFeeds |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx)) let feeds = ctx.WebLog.Rss.CustomFeeds |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx))
return! return!
Views.Admin.webLogSettings Views.WebLog.webLogSettings
(SettingsModel.FromWebLog ctx.WebLog) themes pages uploads (EditRssModel.FromRssOptions ctx.WebLog.Rss) (SettingsModel.FromWebLog ctx.WebLog) themes pages uploads (EditRssModel.FromRssOptions ctx.WebLog.Rss)
feeds feeds
|> adminPage "Web Log Settings" true next ctx |> adminPage "Web Log Settings" true next ctx

View File

@ -2,7 +2,6 @@
module MyWebLog.Handlers.Feed module MyWebLog.Handlers.Feed
open System open System
open System.Collections.Generic
open System.IO open System.IO
open System.Net open System.Net
open System.ServiceModel.Syndication open System.ServiceModel.Syndication
@ -446,7 +445,7 @@ let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next
{ Name = string Newsletter; Value = "Newsletter" } { Name = string Newsletter; Value = "Newsletter" }
{ Name = string Blog; Value = "Blog" } { 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 |> adminPage $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed""" true next ctx
| None -> Error.notFound 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 | 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 deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
match! data.WebLog.FindById ctx.WebLog.Id with match! data.WebLog.FindById ctx.WebLog.Id with

View File

@ -114,7 +114,6 @@ let router : HttpHandler = choose [
route "/administration" >=> Admin.Dashboard.admin route "/administration" >=> Admin.Dashboard.admin
subRoute "/categor" (requireAccess WebLogAdmin >=> choose [ subRoute "/categor" (requireAccess WebLogAdmin >=> choose [
route "ies" >=> Admin.Category.all route "ies" >=> Admin.Category.all
route "ies/bare" >=> Admin.Category.bare
routef "y/%s/edit" Admin.Category.edit routef "y/%s/edit" Admin.Category.edit
]) ])
route "/dashboard" >=> Admin.Dashboard.user route "/dashboard" >=> Admin.Dashboard.user
@ -188,7 +187,6 @@ let router : HttpHandler = choose [
subRoute "/rss" (choose [ subRoute "/rss" (choose [
route "" >=> Feed.saveSettings route "" >=> Feed.saveSettings
route "/save" >=> Feed.saveCustomFeed route "/save" >=> Feed.saveCustomFeed
routef "/%s/delete" Feed.deleteCustomFeed
]) ])
subRoute "/redirect-rules" (choose [ subRoute "/redirect-rules" (choose [
routef "/%i" Admin.RedirectRules.save routef "/%i" Admin.RedirectRules.save
@ -202,13 +200,10 @@ let router : HttpHandler = choose [
route "/new" >=> Admin.Theme.save route "/new" >=> Admin.Theme.save
routef "/%s/delete" Admin.Theme.delete routef "/%s/delete" Admin.Theme.delete
]) ])
subRoute "/upload" (choose [ route "/upload/save" >=> Upload.save
route "/save" >=> Upload.save
routexp "/delete/(.*)" Upload.deleteFromDisk
routef "/%s/delete" Upload.deleteFromDb
])
] ]
DELETE >=> validateCsrf >=> choose [ DELETE >=> validateCsrf >=> choose [
routef "/category/%s" Admin.Category.delete
subRoute "/page" (choose [ subRoute "/page" (choose [
routef "/%s" Page.delete routef "/%s" Page.delete
routef "/%s/revision/%s" Page.deleteRevision routef "/%s/revision/%s" Page.deleteRevision
@ -221,9 +216,14 @@ let router : HttpHandler = choose [
routef "/%s/revisions" Post.purgeRevisions routef "/%s/revisions" Post.purgeRevisions
]) ])
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [ subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
routef "/user/%s" User.delete
routef "/redirect-rules/%i" Admin.RedirectRules.delete routef "/redirect-rules/%i" Admin.RedirectRules.delete
routef "/rss/%s" Feed.deleteCustomFeed
routef "/tag-mapping/%s" Admin.TagMapping.delete 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
]) ])
] ]
]) ])

View File

@ -108,30 +108,24 @@ let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
Path = file.Replace($"{path}{slash}", "").Replace(name, "").Replace(slash, '/') Path = file.Replace($"{path}{slash}", "").Replace(name, "").Replace(slash, '/')
UpdatedOn = create UpdatedOn = create
Source = string Disk }) Source = string Disk })
|> List.ofSeq
with with
| :? DirectoryNotFoundException -> [] // This is fine | :? DirectoryNotFoundException -> [] // This is fine
| ex -> | ex ->
warn "Upload" ctx $"Encountered {ex.GetType().Name} listing uploads for {path}:\n{ex.Message}" 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! return!
hashForPage "Uploaded Files" dbUploads
|> withAntiCsrf ctx |> Seq.ofList
|> addToHash "files" allFiles |> Seq.map (DisplayUpload.FromUpload webLog Database)
|> adminView "upload-list" next ctx |> Seq.append diskUploads
|> Seq.sortByDescending (fun file -> file.UpdatedOn, file.Path)
|> Views.WebLog.uploadList
|> adminPage "Uploaded Files" true next ctx
} }
// GET /admin/upload/new // GET /admin/upload/new
let showNew : HttpHandler = requireAccess Author >=> fun next ctx -> let showNew : HttpHandler = requireAccess Author >=> fun next ctx ->
hashForPage "Upload a File" adminPage "Upload a File" true next ctx Views.WebLog.uploadNew
|> withAntiCsrf ctx
|> addToHash "destination" (string ctx.WebLog.Uploads)
|> adminView "upload-new" next ctx
/// Redirect to the upload list /// 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 return! RequestErrors.BAD_REQUEST "Bad request; no file present" next ctx
} }
// POST /admin/upload/{id}/delete // DELETE /admin/upload/{id}
let deleteFromDb upId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteFromDb upId : HttpHandler = fun next ctx -> task {
match! ctx.Data.Upload.Delete (UploadId upId) ctx.WebLog.Id with match! ctx.Data.Upload.Delete (UploadId upId) ctx.WebLog.Id with
| Ok fileName -> | Ok fileName ->
do! addMessage ctx { UserMessage.Success with Message = $"{fileName} deleted successfully" } 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) path <- String.Join(slash, path.Split slash |> Array.rev |> Array.skip 1 |> Array.rev)
else finished <- true else finished <- true
// POST /admin/upload/delete/{**path} // DELETE /admin/upload/disk/{**path}
let deleteFromDisk urlParts : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteFromDisk urlParts : HttpHandler = fun next ctx -> task {
let filePath = urlParts |> Seq.skip 1 |> Seq.head let filePath = urlParts |> Seq.skip 1 |> Seq.head
let path = Path.Combine(uploadDir, ctx.WebLog.Slug, filePath) let path = Path.Combine(uploadDir, ctx.WebLog.Slug, filePath)
if File.Exists path then if File.Exists path then

View File

@ -14,6 +14,7 @@
<Compile Include="Views\Page.fs" /> <Compile Include="Views\Page.fs" />
<Compile Include="Views\Post.fs" /> <Compile Include="Views\Post.fs" />
<Compile Include="Views\User.fs" /> <Compile Include="Views\User.fs" />
<Compile Include="Views\WebLog.fs" />
<Compile Include="Handlers\Helpers.fs" /> <Compile Include="Handlers\Helpers.fs" />
<Compile Include="Handlers\Admin.fs" /> <Compile Include="Handlers\Admin.fs" />
<Compile Include="Handlers\Feed.fs" /> <Compile Include="Handlers\Feed.fs" />

View File

@ -2,492 +2,106 @@ module MyWebLog.Views.Admin
open Giraffe.Htmx.Common open Giraffe.Htmx.Common
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx open Giraffe.ViewEngine.Htmx
open MyWebLog open MyWebLog
open MyWebLog.ViewModels open MyWebLog.ViewModels
/// The main dashboard /// The administrator dashboard
let dashboard (model: DashboardModel) app = [ let dashboard (themes: Theme list) app = [
h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " &bull; Dashboard" ] let templates = TemplateCache.allNames ()
article [ _class "container" ] [ let cacheBaseUrl = relUrl app "admin/cache/"
div [ _class "row" ] [ let webLogCacheUrl = $"{cacheBaseUrl}web-log/"
section [ _class "col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3" ] [ let themeCacheUrl = $"{cacheBaseUrl}theme/"
div [ _class "card" ] [ let webLogDetail (webLog: WebLog) =
header [ _class "card-header text-white bg-primary" ] [ raw "Posts" ] let refreshUrl = $"{webLogCacheUrl}{webLog.Id}/refresh"
div [ _class "card-body" ] [ div [ _class "row mwl-table-detail" ] [
h6 [ _class "card-subtitle text-muted pb-3" ] [
raw "Published "
span [ _class "badge rounded-pill bg-secondary" ] [ raw (string model.Posts) ]
raw "&nbsp; 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 "&nbsp; 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 "&nbsp; 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" ] [ div [ _class "col" ] [
a [ _href (relUrl app "admin/settings#rss-settings") ] [ raw "&laquo; Back to Settings" ] txt webLog.Name; br []
]
]
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} &rang; ")
|> String.concat ""
{ Name = c.Id; Value = $"{parents} {c.Name}".Trim() })
|> Seq.append [ { Name = ""; Value = "&ndash; Select Category &ndash;" } ]
|> 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 ] ]
]
]
]
/// 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 " &nbsp; "
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 [] [ small [] [
let ruleUrl = relUrl app $"admin/settings/redirect-rules/{idx}" span [ _class "text-muted" ] [ raw webLog.UrlBase ]; br []
a [ _href ruleUrl; _hxTarget $"#{ruleId}"; _hxSwap $"{HxSwap.InnerHtml} show:#{ruleId}:top" ] [ a [ _href refreshUrl; _hxPost refreshUrl ] [ raw "Refresh" ]
raw "Edit"
]
if idx > 0 then
span [ _class "text-muted" ] [ raw " &bull; " ]
a [ _href $"{ruleUrl}/up"; _hxPost $"{ruleUrl}/up" ] [ raw "Move Up" ]
if idx <> model.Length - 1 then
span [ _class "text-muted" ] [ raw " &bull; " ]
a [ _href $"{ruleUrl}/down"; _hxPost $"{ruleUrl}/down" ] [ raw "Move Down" ]
span [ _class "text-muted" ] [ raw " &bull; " ]
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 ] let themeDetail (theme: Theme) =
div [ _class "col-2 text-center" ] [ yesOrNo rule.IsRegex ] 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 " &bull; " ]
a [ _href refreshUrl; _hxPost refreshUrl ] [ raw "Refresh" ]
] ]
]
div [ _class "col-4" ] [
raw (templates |> List.filter _.StartsWith(string theme.Id) |> List.length |> string)
]
]
h2 [ _class "my-3" ] [ raw app.PageTitle ] h2 [ _class "my-3" ] [ raw app.PageTitle ]
article [] [ article [] [
p [ _class "mb-3" ] [ fieldset [ _class "container mb-3 pb-0" ] [
a [ _href (relUrl app "admin/settings") ] [ raw "&laquo; Back to Settings" ] legend [] [ raw "Themes" ]
span [ _hxGet (relUrl app "admin/theme/list"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml ] []
] ]
div [ _class "container" ] [ fieldset [ _class "container mb-3 pb-0" ] [
div [ _class "row" ] [ legend [] [ raw "Caches" ]
div [ _class "col" ] [ p [ _class "pb-2" ] [
a [ _href (relUrl app "admin/settings/redirect-rules/-1"); _class "btn btn-primary btn-sm mb-3" raw "myWebLog uses a few caches to ensure that it serves pages as fast as possible. ("
_hxTarget "#rule_new" ] [ a [ _href "https://bitbadger.solutions/open-source/myweblog/advanced.html#cache-management"
raw "Add Redirect Rule"
]
]
]
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"
]
]
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" ] [ _target "_blank" ] [
raw "read and understand the documentation on this feature" raw "more information"
]; raw ")"
] ]
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 " &nbsp; "
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"
]
span [ _class "text-muted" ] [ raw " &bull; " ]
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 "row" ] [
div [ _class "col" ] [ div [ _class "col-12 col-lg-6 pb-3" ] [
if List.isEmpty model then div [ _class "card" ] [
div [ _id "tag_new" ] [ header [ _class "card-header text-white bg-secondary" ] [ raw "Web Logs" ]
p [ _class "text-muted text-center fst-italic" ] [ raw "This web log has no tag mappings" ] 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"
] ]
else let webLogUrl = $"{cacheBaseUrl}web-log/"
div [ _class "container g-0" ] [ form [ _method "post"; _class "container g-0"; _hxNoBoost; _hxTarget "body"
div [ _class "row mwl-table-heading" ] [ _hxSwap $"{HxSwap.InnerHtml} show:window:top" ] [
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 antiCsrf app
div [ _class "row mwl-table-detail"; _id "tag_new" ] [] button [ _type "submit"; _class "btn btn-sm btn-primary mb-2"
yield! List.map tagMapDetail model _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
] ]
] ]
] ]
] ]
|> List.singleton 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 themes /// Display a list of themes
let themeList (model: DisplayTheme list) app = let themeList (model: DisplayTheme list) app =
@ -574,185 +188,3 @@ let themeUpload app =
] ]
] ]
|> List.singleton |> 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 " &bull; "
a [ _href "#rss-settings" ] [ raw "RSS Settings" ]; raw " &bull; "
a [ _href "#tag-mappings" ] [ raw "Tag Mappings" ]; raw " &bull; "
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 &ldquo;0&rdquo; to use &ldquo;Posts per Page&rdquo; 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 " &nbsp; "; 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 " &bull; " ]
a [ _href $"{feedUrl}/edit" ] [ raw "Edit" ]
span [ _class "text-muted" ] [ raw " &bull; " ]
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 ] []
]
]
]

View File

@ -74,6 +74,9 @@ let txt = encodedText
/// Shorthand for raw text in a template /// Shorthand for raw text in a template
let raw = rawText 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 /// The pattern for a long date
let longDatePattern = let longDatePattern =
ZonedDateTimePattern.CreateWithInvariantCulture("MMMM d, yyyy", DateTimeZoneProviders.Tzdb) ZonedDateTimePattern.CreateWithInvariantCulture("MMMM d, yyyy", DateTimeZoneProviders.Tzdb)
@ -136,7 +139,7 @@ let passwordField attrs name labelText value extra =
/// Create a select (dropdown) field /// Create a select (dropdown) field
let selectField<'T, 'a> 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" ] [ div [ _class "form-floating" ] [
select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [ select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [
for item in values do for item in values do
@ -161,6 +164,10 @@ let checkboxSwitch attrs name labelText (value: bool) extra =
let saveButton = let saveButton =
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ] 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 " &bull; " ]
/// Functions for generating content in varying layouts /// Functions for generating content in varying layouts
module Layout = module Layout =

View File

@ -66,7 +66,7 @@ let chapterEdit (model: EditChapterModel) app = [
textField (_required :: attrs) (nameof model.LocationGeo) "Geo URL" model.LocationGeo [ textField (_required :: attrs) (nameof model.LocationGeo) "Geo URL" model.LocationGeo [
em [ _class "form-text" ] [ em [ _class "form-text" ] [
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended" 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" raw "see spec"
] ]
] ]
@ -76,10 +76,10 @@ let chapterEdit (model: EditChapterModel) app = [
textField attrs (nameof model.LocationOsm) "OpenStreetMap ID" model.LocationOsm [ textField attrs (nameof model.LocationOsm) "OpenStreetMap ID" model.LocationOsm [
em [ _class "form-text" ] [ em [ _class "form-text" ] [
raw "Optional; " 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 ", " raw ", "
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#osm-recommended" 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" raw "see spec"
] ]
] ]

View File

@ -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} &nbsp; &raquo; ")
|> String.concat ""
{ Name = c.Id; Value = $"{parents}{c.Name}" })
|> Seq.append [ { Name = ""; Value = "&ndash; None &ndash;" } ]
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} &rang; ")
|> 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 " &bull; 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 "&nbsp; 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 "&nbsp; 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 "&nbsp; 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 "&laquo; 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} &rang; ")
|> String.concat ""
{ Name = c.Id; Value = $"{parents}{c.Name}" })
|> Seq.append [ { Name = ""; Value = "&ndash; Select Category &ndash;" } ]
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 " &nbsp; "
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 "&laquo; 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 " &nbsp; "
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 " &bull; "
a [ _href "#rss-settings" ] [ raw "RSS Settings" ]; raw " &bull; "
a [ _href "#tag-mappings" ] [ raw "Tag Mappings" ]; raw " &bull; "
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 &ldquo;0&rdquo; to use &ldquo;Posts per Page&rdquo; 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 " &nbsp; "; 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 ] []
]
]
]

View File

@ -1,92 +0,0 @@
<h2 class=my-3>{{ page_title }}</h2>
<article>
<fieldset class="container mb-3 pb-0">
<legend>Themes</legend>
<span hx-get="{{ "admin/theme/list" | relative_link }}" hx-trigger="load" hx-swap="outerHTML"></span>
</fieldset>
<fieldset class="container mb-3 pb-0">
{%- assign cache_base_url = "admin/cache/" -%}
<legend>Caches</legend>
<div class="row pb-2">
<div class=col>
<p>
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>more information</a>)
</div>
</div>
<div class=row>
<div class="col-12 col-lg-6 pb-3">
<div class=card>
<header class="card-header text-white bg-secondary">Web Logs</header>
<div class="card-body pb-0">
<h6 class="card-subtitle text-muted pb-3">
These caches include the page list and categories for each web log
</h6>
{%- assign web_log_base_url = cache_base_url | append: "web-log/" -%}
<form method=post class="container g-0" hx-boost=false hx-target=body hx-swap="innerHTML show:window:top">
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<button type=submit class="btn btn-sm btn-primary mb-2"
hx-post="{{ web_log_base_url | append: "all/refresh" | relative_link }}">
Refresh All
</button>
<div class="row mwl-table-heading">
<div class=col>Web Log</div>
</div>
{%- for web_log in web_logs %}
<div class="row mwl-table-detail">
<div class=col>
{{ web_log[1] }}<br>
<small>
<span class=text-muted>{{ web_log[2] }}</span><br>
{%- assign refresh_url = web_log_base_url | append: web_log[0] | append: "/refresh" | relative_link -%}
<a href="{{ refresh_url }}" hx-post="{{ refresh_url }}">Refresh</a>
</small>
</div>
</div>
{%- endfor %}
</form>
</div>
</div>
</div>
<div class="col-12 col-lg-6 pb-3">
<div class=card>
<header class="card-header text-white bg-secondary">Themes</header>
<div class="card-body pb-0">
<h6 class="card-subtitle text-muted pb-3">
The theme template cache is filled on demand as pages are displayed; refreshing a theme with no cached
templates will still refresh its asset cache
</h6>
{%- assign theme_base_url = cache_base_url | append: "theme/" -%}
<form method=post class="container g-0" hx-boost=false hx-target=body hx-swap="innerHTML show:window:top">
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<button type=submit class="btn btn-sm btn-primary mb-2"
hx-post="{{ theme_base_url | append: "all/refresh" | relative_link }}">
Refresh All
</button>
<div class="row mwl-table-heading">
<div class=col-8>Theme</div>
<div class=col-4>Cached</div>
</div>
{%- for theme in cached_themes %}
{% unless theme[0] == "admin" %}
<div class="row mwl-table-detail">
<div class=col-8>
{{ theme[1] }}<br>
<small>
<span class=text-muted>{{ theme[0] }} &bull; </span>
{%- assign refresh_url = theme_base_url | append: theme[0] | append: "/refresh" | relative_link -%}
<a href="{{ refresh_url }}" hx-post="{{ refresh_url }}">Refresh</a>
</small>
</div>
<div class=col-4>{{ theme[2] }}</div>
</div>
{% endunless %}
{%- endfor %}
</form>
</div>
</div>
</div>
</div>
</fieldset>
</article>

View File

@ -1,52 +0,0 @@
<div class=col-12>
<h5 class=my-3>{{ page_title }}</h5>
<form hx-post="{{ "admin/category/save" | relative_link }}" method=post class=container hx-target=#catList
hx-swap="outerHTML show:window:top">
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type=hidden name=CategoryId value="{{ model.category_id }}">
<div class=row>
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
<div class=form-floating>
<input type=text name=Name id=name class=form-control placeholder=Name autofocus required
value="{{ model.name | escape }}">
<label for=name>Name</label>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 mb-3">
<div class=form-floating>
<input type=text name=Slug id=slug class=form-control placeholder=Slug required
value="{{ model.slug | escape }}">
<label for=slug>Slug</label>
</div>
</div>
<div class="col-12 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
<div class=form-floating>
<select name=ParentId id=parentId class=form-control>
<option value=""{% if model.parent_id == "" %} selected{% endif %}>&ndash; None &ndash;</option>
{% for cat in categories -%}
{%- unless cat.id == model.category_id %}
<option value="{{ cat.id }}"{% if model.parent_id == cat.id %} selected{% endif %}>
{% for it in cat.parent_names %} &nbsp; &raquo; {% endfor %}{{ cat.name }}
</option>
{% endunless -%}
{%- endfor %}
</select>
<label for=parentId>Parent Category</label>
</div>
</div>
<div class="col-12 col-xl-10 offset-xl-1 mb-3">
<div class=form-floating>
<input name=Description id=description class=form-control
placeholder="A short description of this category" value="{{ model.description | escape }}">
<label for=description>Description</label>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col text-center">
<button type=submit class="btn btn-sm btn-primary">Save Changes</button>
<a href="{{ "admin/categories/bare" | relative_link }}" class="btn btn-sm btn-secondary ms-3">Cancel</a>
</div>
</div>
</form>
</div>

View File

@ -1,57 +0,0 @@
<div id=catList class=container>
<div class=row>
<div class=col>
{%- 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" -%}
<div class=container>
<div class="row mwl-table-heading">
<div class="{{ cat_col }}">Category<span class=d-md-none>; Description</span></div>
<div class="{{ desc_col }} d-none d-md-inline-block">Description</div>
</div>
</div>
<form method=post class=container hx-target=#catList hx-swap="outerHTML show:window:top">
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="row mwl-table-detail" id="cat_new"></div>
{% for cat in categories -%}
<div class="row mwl-table-detail" id="cat_{{ cat.id }}">
<div class="{{ cat_col }} no-wrap">
{%- if cat.parent_names %}
<small class=text-muted>{% for name in cat.parent_names %}{{ name }} &rang; {% endfor %}</small>
{%- endif %}
{{ cat.name }}<br>
<small>
{%- assign cat_url_base = "admin/category/" | append: cat.id -%}
{%- if cat.post_count > 0 %}
<a href="{{ cat | category_link }}" target=_blank>
View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%}
</a>
<span class=text-muted> &bull; </span>
{%- endif %}
<a href="{{ cat_url_base | append: "/edit" | relative_link }}" hx-target="#cat_{{ cat.id }}"
hx-swap="innerHTML show:#cat_{{ cat.id }}:top">
Edit
</a>
<span class=text-muted> &bull; </span>
{%- assign cat_del_link = cat_url_base | append: "/delete" | relative_link -%}
<a href="{{ cat_del_link }}" hx-post="{{ cat_del_link }}" class=text-danger
hx-confirm="Are you sure you want to delete the category &ldquo;{{ cat.name }}&rdquo;? This action cannot be undone.">
Delete
</a>
</small>
</div>
<div class="{{ desc_col }}">
{%- if cat.description %}{{ cat.description.value }}{% else %}<em class=text-muted>none</em>{% endif %}
</div>
</div>
{%- endfor %}
</form>
{%- else -%}
<div id=cat_new>
<p class="text-muted fst-italic text-center">This web log has no categories defined</p>
</div>
{%- endif %}
</div>
</div>
</div>

View File

@ -1,8 +0,0 @@
<h2 class=my-3>{{ page_title }}</h2>
<article>
<a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
hx-target=#cat_new>
Add a New Category
</a>
{{ category_list }}
</article>

View File

@ -1,75 +0,0 @@
<h2 class=my-3>{{ page_title }}</h2>
<article>
{%- capture base_url %}{{ "" | relative_link }}{% endcapture -%}
{%- capture upload_path %}upload/{{ web_log.slug }}/{% endcapture -%}
{%- capture upload_base %}{{ base_url }}{{ upload_path }}{% endcapture -%}
<a href="{{ "admin/upload/new" | relative_link }}" class="btn btn-primary btn-sm mb-3">Upload a New File</a>
<form method=post class=container hx-target=body>
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class=row>
<div class="col text-center"><em class=text-muted>Uploaded files served from</em><br>{{ upload_base }}</div>
</div>
{%- assign file_count = files | size -%}
{%- if file_count > 0 %}
<div class="row mwl-table-heading">
<div class=col-6>File Name</div>
<div class=col-3>Path</div>
<div class=col-3>File Date/Time</div>
</div>
{% for file in files %}
<div class="row mwl-table-detail">
<div class=col-6>
{%- capture badge_class -%}
{%- if file.source == "Disk" %}secondary{% else %}primary{% endif -%}
{%- endcapture -%}
{%- assign path_and_name = file.path | append: file.name -%}
{%- assign blog_rel = upload_path | append: path_and_name -%}
<span class="badge bg-{{ badge_class }} text-uppercase float-end mt-1">{{ file.source }}</span>
{{ file.name }}<br>
<small>
<a href="{{ upload_base | append: path_and_name }}" target="_blank">View File</a>
<span class=text-muted> &bull; Copy </span>
<a href="{{ blog_rel | absolute_link }}" hx-boost=false
onclick="return Admin.copyText('{{ blog_rel | absolute_link }}', this)">
Absolute
</a>
<span class=text-muted> | </span>
<a href="{{ blog_rel | relative_link }}" hx-boost=false
onclick="return Admin.copyText('{{ blog_rel | relative_link }}', this)">
Relative
</a>
{%- unless base_url == "/" %}
<span class=text-muted> | </span>
<a href="{{ blog_rel }}" hx-boost=false onclick="return Admin.copyText('/{{ blog_rel }}', this)">
For Post
</a>
{%- endunless %}
<span class=text-muted> Link</span>
{% if is_web_log_admin %}
<span class=text-muted> &bull; </span>
{%- capture delete_url -%}
{%- if file.source == "Disk" -%}
admin/upload/delete/{{ path_and_name }}
{%- else -%}
admin/upload/{{ file.id }}/delete
{%- endif -%}
{%- endcapture -%}
<a href="{{ delete_url | relative_link }}" hx-post="{{ delete_url | relative_link }}"
hx-confirm="Are you sure you want to delete {{ file.name }}? This action cannot be undone."
class=text-danger>Delete</a>
{% endif %}
</small>
</div>
<div class=col-3>{{ file.path }}</div>
<div class=col-3>
{% if file.updated_on %}{{ file.updated_on.value | date: "yyyy-MM-dd/HH:mm" }}{% else %}--{% endif %}
</div>
</div>
{% endfor %}
{%- else -%}
<div class=row>
<div class="col text-muted fst-italic text-center"><br>This web log has uploaded files</div>
</div>
{%- endif %}
</form>
</article>

View File

@ -1,29 +0,0 @@
<h2>{{ page_title }}</h2>
<article>
<form action="{{ "admin/upload/save" | relative_link }}" method=post class=container enctype=multipart/form-data
hx-boost=false>
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<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>File to Upload</label>
</div>
</div>
<div class="col-12 col-md-6 pb-3 d-flex align-self-center justify-content-around">
Destination<br>
<div class=btn-group role=group aria-label="Upload destination button group">
<input type=radio name=Destination id=destination_db class=btn-check value=Database
{%- if destination == "Database" %} checked{% endif %}>
<label class="btn btn-outline-primary" for=destination_db>Database</label>
<input type=radio name=Destination id=destination_disk class=btn-check value=Disk
{%- if destination == "Disk" %} checked{% endif %}>
<label class="btn btn-outline-secondary" for=destination_disk>Disk</label>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col text-center"><button type=submit class="btn btn-primary">Upload File</button></div>
</div>
</form>
</article>