Version 2.1 #41
@ -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,25 +95,8 @@ 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 {
|
||||||
@ -150,10 +111,8 @@ module Category =
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,12 +134,12 @@ module Category =
|
|||||||
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
@ -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
|
||||||
|
@ -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" />
|
||||||
|
@ -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 " • 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 " 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" ] [
|
div [ _class "col" ] [
|
||||||
a [ _href (relUrl app "admin/settings#rss-settings") ] [ raw "« 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} ⟩ ")
|
|
||||||
|> 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 ] ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 [] [
|
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 " • " ]
|
|
||||||
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 ]
|
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 " • " ]
|
||||||
|
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 "« 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 " "
|
|
||||||
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 " • " ]
|
|
||||||
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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|> List.singleton
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 " • "
|
|
||||||
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 ] []
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
@ -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 " • " ]
|
||||||
|
|
||||||
/// Functions for generating content in varying layouts
|
/// Functions for generating content in varying layouts
|
||||||
module Layout =
|
module Layout =
|
||||||
|
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
875
src/MyWebLog/Views/WebLog.fs
Normal file
875
src/MyWebLog/Views/WebLog.fs
Normal 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} » ")
|
||||||
|
|> 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 ] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
@ -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] }} • </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>
|
|
@ -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 %}>– None –</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 %} » {% 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>
|
|
@ -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 }} ⟩ {% 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> • </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> • </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 “{{ cat.name }}”? 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>
|
|
@ -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>
|
|
@ -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> • 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> • </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>
|
|
@ -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>
|
|
Loading…
Reference in New Issue
Block a user