- Add autoHtmx field / htmx partial support
- Add page_foot tag for scripts - Add htmx to admin area - Move create/permalink import to its own module - Add htmx to tech-blog theme - Move dashboard to admin/dashboard
This commit is contained in:
parent
019ac229fb
commit
1fd2bfd08e
@ -752,6 +752,7 @@ module WebLog =
|
||||
"postsPerPage", webLog.postsPerPage
|
||||
"timeZone", webLog.timeZone
|
||||
"themePath", webLog.themePath
|
||||
"autoHtmx", webLog.autoHtmx
|
||||
]
|
||||
write; withRetryDefault; ignoreResult
|
||||
}
|
||||
|
@ -275,6 +275,9 @@ type WebLog =
|
||||
|
||||
/// The RSS options for this web log
|
||||
rss : RssOptions
|
||||
|
||||
/// Whether to automatically load htmx
|
||||
autoHtmx : bool
|
||||
}
|
||||
|
||||
/// Functions to support web logs
|
||||
@ -291,6 +294,7 @@ module WebLog =
|
||||
urlBase = ""
|
||||
timeZone = ""
|
||||
rss = RssOptions.empty
|
||||
autoHtmx = false
|
||||
}
|
||||
|
||||
/// Get the host (including scheme) and extra path from the URL base
|
||||
|
@ -676,7 +676,11 @@ type SettingsModel =
|
||||
|
||||
/// The theme to use to display the web log
|
||||
themePath : string
|
||||
|
||||
/// Whether to automatically load htmx
|
||||
autoHtmx : bool
|
||||
}
|
||||
|
||||
/// Create a settings model from a web log
|
||||
static member fromWebLog (webLog : WebLog) =
|
||||
{ name = webLog.name
|
||||
@ -685,6 +689,19 @@ type SettingsModel =
|
||||
postsPerPage = webLog.postsPerPage
|
||||
timeZone = webLog.timeZone
|
||||
themePath = webLog.themePath
|
||||
autoHtmx = webLog.autoHtmx
|
||||
}
|
||||
|
||||
/// Update a web log with settings from the form
|
||||
member this.update (webLog : WebLog) =
|
||||
{ webLog with
|
||||
name = this.name
|
||||
subtitle = if this.subtitle = "" then None else Some this.subtitle
|
||||
defaultPage = this.defaultPage
|
||||
postsPerPage = this.postsPerPage
|
||||
timeZone = this.timeZone
|
||||
themePath = this.themePath
|
||||
autoHtmx = this.autoHtmx
|
||||
}
|
||||
|
||||
|
||||
|
@ -5,12 +5,17 @@ open System
|
||||
open System.IO
|
||||
open System.Web
|
||||
open DotLiquid
|
||||
open Giraffe.ViewEngine
|
||||
open MyWebLog.ViewModels
|
||||
|
||||
/// Get the current web log from the DotLiquid context
|
||||
let webLog (ctx : Context) =
|
||||
ctx.Environments[0].["web_log"] :?> WebLog
|
||||
|
||||
/// Does an asset exist for the current theme?
|
||||
let assetExists fileName (webLog : WebLog) =
|
||||
File.Exists (Path.Combine ("wwwroot", "themes", webLog.themePath, fileName))
|
||||
|
||||
/// Obtain the link from known types
|
||||
let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) =
|
||||
match item with
|
||||
@ -72,9 +77,11 @@ type EditPostLinkFilter () =
|
||||
type NavLinkFilter () =
|
||||
static member NavLink (ctx : Context, url : string, text : string) =
|
||||
let webLog = webLog ctx
|
||||
let _, path = WebLog.hostAndPath webLog
|
||||
let path = if path = "" then path else $"{path.Substring 1}/"
|
||||
seq {
|
||||
"<li class=\"nav-item\"><a class=\"nav-link"
|
||||
if url = string ctx.Environments[0].["current_page"] then " active"
|
||||
if (string ctx.Environments[0].["current_page"]).StartsWith $"{path}{url}" then " active"
|
||||
"\" href=\""
|
||||
WebLog.relativeUrl webLog (Permalink url)
|
||||
"\">"
|
||||
@ -98,10 +105,9 @@ type PageHeadTag () =
|
||||
result.WriteLine $"""<meta name="generator" content="{context.Environments[0].["generator"]}">"""
|
||||
|
||||
// Theme assets
|
||||
let has fileName = File.Exists (Path.Combine ("wwwroot", "themes", webLog.themePath, fileName))
|
||||
if has "style.css" then
|
||||
if assetExists "style.css" webLog then
|
||||
result.WriteLine $"""{s}<link rel="stylesheet" href="/themes/{webLog.themePath}/style.css">"""
|
||||
if has "favicon.ico" then
|
||||
if assetExists "favicon.ico" webLog then
|
||||
result.WriteLine $"""{s}<link rel="icon" href="/themes/{webLog.themePath}/favicon.ico">"""
|
||||
|
||||
// RSS feeds and canonical URLs
|
||||
@ -132,6 +138,22 @@ type PageHeadTag () =
|
||||
let url = WebLog.absoluteUrl webLog (Permalink page.permalink)
|
||||
result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""
|
||||
|
||||
|
||||
/// Create various items in the page header based on the state of the page being generated
|
||||
type PageFootTag () =
|
||||
inherit Tag ()
|
||||
|
||||
override this.Render (context : Context, result : TextWriter) =
|
||||
let webLog = webLog context
|
||||
// spacer
|
||||
let s = " "
|
||||
|
||||
if webLog.autoHtmx then
|
||||
result.WriteLine $"{s}{RenderView.AsString.htmlNode Htmx.Script.minified}"
|
||||
|
||||
if assetExists "script.js" webLog then
|
||||
result.WriteLine $"""{s}<script src="/themes/{webLog.themePath}/script.js"></script>"""
|
||||
|
||||
|
||||
/// A filter to generate a relative link
|
||||
type RelativeLinkFilter () =
|
||||
@ -167,7 +189,7 @@ type UserLinksTag () =
|
||||
"""<ul class="navbar-nav flex-grow-1 justify-content-end">"""
|
||||
match Convert.ToBoolean context.Environments[0].["logged_on"] with
|
||||
| true ->
|
||||
$"""<li class="nav-item"><a class="nav-link" href="{link "admin"}">Dashboard</a></li>"""
|
||||
$"""<li class="nav-item"><a class="nav-link" href="{link "admin/dashboard"}">Dashboard</a></li>"""
|
||||
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>"""
|
||||
| false ->
|
||||
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-on"}">Log On</a></li>"""
|
||||
@ -184,3 +206,31 @@ type ValueFilter () =
|
||||
| None -> $"-- {name} not found --"
|
||||
|
||||
|
||||
open System.Collections.Generic
|
||||
open Microsoft.AspNetCore.Antiforgery
|
||||
|
||||
/// Register custom filters/tags and safe types
|
||||
let register () =
|
||||
[ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter>
|
||||
typeof<NavLinkFilter>; typeof<RelativeLinkFilter>; typeof<TagLinkFilter>; typeof<ThemeAssetFilter>
|
||||
typeof<ValueFilter>
|
||||
]
|
||||
|> List.iter Template.RegisterFilter
|
||||
|
||||
Template.RegisterTag<PageHeadTag> "page_head"
|
||||
Template.RegisterTag<PageFootTag> "page_foot"
|
||||
Template.RegisterTag<UserLinksTag> "user_links"
|
||||
|
||||
[ // Domain types
|
||||
typeof<CustomFeed>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>; typeof<TagMap>; typeof<WebLog>
|
||||
// View models
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
||||
typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
||||
typeof<UserMessage>
|
||||
// Framework types
|
||||
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
||||
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||
]
|
||||
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
|
||||
|
@ -291,19 +291,11 @@ let saveSettings : HttpHandler = fun next ctx -> task {
|
||||
let! model = ctx.BindFormAsync<SettingsModel> ()
|
||||
match! Data.WebLog.findById webLog.id conn with
|
||||
| Some webLog ->
|
||||
let updated =
|
||||
{ webLog with
|
||||
name = model.name
|
||||
subtitle = if model.subtitle = "" then None else Some model.subtitle
|
||||
defaultPage = model.defaultPage
|
||||
postsPerPage = model.postsPerPage
|
||||
timeZone = model.timeZone
|
||||
themePath = model.themePath
|
||||
}
|
||||
do! Data.WebLog.updateSettings updated conn
|
||||
let webLog = model.update webLog
|
||||
do! Data.WebLog.updateSettings webLog conn
|
||||
|
||||
// Update cache
|
||||
WebLogCache.set updated
|
||||
WebLogCache.set webLog
|
||||
|
||||
do! addMessage ctx { UserMessage.success with message = "Web log settings saved successfully" }
|
||||
return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin")) next ctx
|
||||
|
@ -79,6 +79,11 @@ let private deriveWebLogFromHash (hash : Hash) (ctx : HttpContext) =
|
||||
hash["web_log"] :?> WebLog
|
||||
|
||||
open Giraffe
|
||||
open Giraffe.Htmx
|
||||
open Giraffe.ViewEngine
|
||||
|
||||
/// htmx script tag
|
||||
let private htmxScript = RenderView.AsString.htmlNode Htmx.Script.minified
|
||||
|
||||
/// Render a view for the specified theme, using the specified template, layout, and hash
|
||||
let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||
@ -90,6 +95,7 @@ let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
||||
hash.Add ("messages", messages)
|
||||
hash.Add ("generator", generator ctx)
|
||||
hash.Add ("htmx_script", htmxScript)
|
||||
|
||||
do! commitSession ctx
|
||||
|
||||
@ -101,7 +107,9 @@ let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||
hash.Add ("content", contentTemplate.Render hash)
|
||||
|
||||
// ...then render that content with its layout
|
||||
let! layoutTemplate = TemplateCache.get theme "layout"
|
||||
let isHtmx = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
||||
let layout = if isHtmx then "layout-partial" else "layout"
|
||||
let! layoutTemplate = TemplateCache.get theme layout
|
||||
|
||||
return! htmlString (layoutTemplate.Render hash) next ctx
|
||||
}
|
||||
|
@ -95,11 +95,11 @@ let router : HttpHandler = choose [
|
||||
]
|
||||
subRoute "/admin" (requireUser >=> choose [
|
||||
GET >=> choose [
|
||||
route "" >=> Admin.dashboard
|
||||
subRoute "/categor" (choose [
|
||||
route "ies" >=> Admin.listCategories
|
||||
routef "y/%s/edit" Admin.editCategory
|
||||
])
|
||||
route "/dashboard" >=> Admin.dashboard
|
||||
subRoute "/page" (choose [
|
||||
route "s" >=> Admin.listPages 1
|
||||
routef "s/page/%i" Admin.listPages
|
||||
|
@ -56,7 +56,8 @@ let doLogOn : HttpHandler = fun next ctx -> task {
|
||||
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
||||
do! addMessage ctx
|
||||
{ UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" }
|
||||
return! redirectToGet (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin"))) next ctx
|
||||
return! redirectToGet (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin/dashboard")))
|
||||
next ctx
|
||||
| _ ->
|
||||
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" }
|
||||
return! logOn model.returnTo next ctx
|
||||
|
126
src/MyWebLog/Maintenance.fs
Normal file
126
src/MyWebLog/Maintenance.fs
Normal file
@ -0,0 +1,126 @@
|
||||
module MyWebLog.Maintenance
|
||||
|
||||
open System
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
|
||||
open System.IO
|
||||
open RethinkDb.Driver.FSharp
|
||||
|
||||
/// Create the web log information
|
||||
let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
|
||||
|
||||
let conn = sp.GetRequiredService<IConnection> ()
|
||||
|
||||
let timeZone =
|
||||
let local = TimeZoneInfo.Local.Id
|
||||
match TimeZoneInfo.Local.HasIanaId with
|
||||
| true -> local
|
||||
| false ->
|
||||
match TimeZoneInfo.TryConvertWindowsIdToIanaId local with
|
||||
| true, ianaId -> ianaId
|
||||
| false, _ -> raise <| TimeZoneNotFoundException $"Cannot find IANA timezone for {local}"
|
||||
|
||||
// Create the web log
|
||||
let webLogId = WebLogId.create ()
|
||||
let userId = WebLogUserId.create ()
|
||||
let homePageId = PageId.create ()
|
||||
|
||||
do! Data.WebLog.add
|
||||
{ WebLog.empty with
|
||||
id = webLogId
|
||||
name = args[2]
|
||||
urlBase = args[1]
|
||||
defaultPage = PageId.toString homePageId
|
||||
timeZone = timeZone
|
||||
} conn
|
||||
|
||||
// Create the admin user
|
||||
let salt = Guid.NewGuid ()
|
||||
|
||||
do! Data.WebLogUser.add
|
||||
{ WebLogUser.empty with
|
||||
id = userId
|
||||
webLogId = webLogId
|
||||
userName = args[3]
|
||||
firstName = "Admin"
|
||||
lastName = "User"
|
||||
preferredName = "Admin"
|
||||
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
||||
salt = salt
|
||||
authorizationLevel = Administrator
|
||||
} conn
|
||||
|
||||
// Create the default home page
|
||||
do! Data.Page.add
|
||||
{ Page.empty with
|
||||
id = homePageId
|
||||
webLogId = webLogId
|
||||
authorId = userId
|
||||
title = "Welcome to myWebLog!"
|
||||
permalink = Permalink "welcome-to-myweblog.html"
|
||||
publishedOn = DateTime.UtcNow
|
||||
updatedOn = DateTime.UtcNow
|
||||
text = "<p>This is your default home page.</p>"
|
||||
revisions = [
|
||||
{ asOf = DateTime.UtcNow
|
||||
text = Html "<p>This is your default home page.</p>"
|
||||
}
|
||||
]
|
||||
} conn
|
||||
|
||||
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
||||
}
|
||||
|
||||
/// Create a new web log
|
||||
let createWebLog args sp = task {
|
||||
match args |> Array.length with
|
||||
| 5 -> return! doCreateWebLog args sp
|
||||
| _ ->
|
||||
printfn "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
||||
return! System.Threading.Tasks.Task.CompletedTask
|
||||
}
|
||||
|
||||
/// Import prior permalinks from a text files with lines in the format "[old] [new]"
|
||||
let importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
|
||||
let conn = sp.GetRequiredService<IConnection> ()
|
||||
|
||||
match! Data.WebLog.findByHost urlBase conn with
|
||||
| Some webLog ->
|
||||
|
||||
let mapping =
|
||||
File.ReadAllLines file
|
||||
|> Seq.ofArray
|
||||
|> Seq.map (fun it ->
|
||||
let parts = it.Split " "
|
||||
Permalink parts[0], Permalink parts[1])
|
||||
|
||||
for old, current in mapping do
|
||||
match! Data.Post.findByPermalink current webLog.id conn with
|
||||
| Some post ->
|
||||
let! withLinks = rethink<Post> {
|
||||
withTable Data.Table.Post
|
||||
get post.id
|
||||
result conn
|
||||
}
|
||||
do! rethink {
|
||||
withTable Data.Table.Post
|
||||
get post.id
|
||||
update [ "priorPermalinks", old :: withLinks.priorPermalinks :> obj]
|
||||
write; ignoreResult conn
|
||||
}
|
||||
printfn $"{Permalink.toString old} -> {Permalink.toString current}"
|
||||
| None -> printfn $"Cannot find current post for {Permalink.toString current}"
|
||||
printfn "Done!"
|
||||
| None -> printfn $"No web log found at {urlBase}"
|
||||
}
|
||||
|
||||
/// Import permalinks if all is well
|
||||
let importPermalinks args sp = task {
|
||||
match args |> Array.length with
|
||||
| 3 -> return! importPriorPermalinks args[1] args[2] sp
|
||||
| _ ->
|
||||
printfn "Usage: MyWebLog import-permalinks [url] [file-name]"
|
||||
return! System.Threading.Tasks.Task.CompletedTask
|
||||
}
|
@ -17,12 +17,15 @@
|
||||
<Compile Include="Handlers\User.fs" />
|
||||
<Compile Include="Handlers\Routes.fs" />
|
||||
<Compile Include="DotLiquidBespoke.fs" />
|
||||
<Compile Include="Maintenance.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotLiquid" Version="2.2.656" />
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
<PackageReference Include="Giraffe.Htmx" Version="1.7.0" />
|
||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.7.0" />
|
||||
<PackageReference Include="RethinkDB.DistributedCache" Version="1.0.0-rc1" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.4" />
|
||||
<PackageReference Include="System.ServiceModel.Syndication" Version="6.0.0" />
|
||||
|
@ -23,148 +23,17 @@ type WebLogMiddleware (next : RequestDelegate, log : ILogger<WebLogMiddleware>)
|
||||
ctx.Response.StatusCode <- 404
|
||||
}
|
||||
|
||||
|
||||
open System
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
/// Create the default information for a new web log
|
||||
module NewWebLog =
|
||||
|
||||
open System.IO
|
||||
open RethinkDb.Driver.FSharp
|
||||
|
||||
/// Create the web log information
|
||||
let private createWebLog (args : string[]) (sp : IServiceProvider) = task {
|
||||
|
||||
let conn = sp.GetRequiredService<IConnection> ()
|
||||
|
||||
let timeZone =
|
||||
let local = TimeZoneInfo.Local.Id
|
||||
match TimeZoneInfo.Local.HasIanaId with
|
||||
| true -> local
|
||||
| false ->
|
||||
match TimeZoneInfo.TryConvertWindowsIdToIanaId local with
|
||||
| true, ianaId -> ianaId
|
||||
| false, _ -> raise <| TimeZoneNotFoundException $"Cannot find IANA timezone for {local}"
|
||||
|
||||
// Create the web log
|
||||
let webLogId = WebLogId.create ()
|
||||
let userId = WebLogUserId.create ()
|
||||
let homePageId = PageId.create ()
|
||||
|
||||
do! Data.WebLog.add
|
||||
{ WebLog.empty with
|
||||
id = webLogId
|
||||
name = args[2]
|
||||
urlBase = args[1]
|
||||
defaultPage = PageId.toString homePageId
|
||||
timeZone = timeZone
|
||||
} conn
|
||||
|
||||
// Create the admin user
|
||||
let salt = Guid.NewGuid ()
|
||||
|
||||
do! Data.WebLogUser.add
|
||||
{ WebLogUser.empty with
|
||||
id = userId
|
||||
webLogId = webLogId
|
||||
userName = args[3]
|
||||
firstName = "Admin"
|
||||
lastName = "User"
|
||||
preferredName = "Admin"
|
||||
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
||||
salt = salt
|
||||
authorizationLevel = Administrator
|
||||
} conn
|
||||
|
||||
// Create the default home page
|
||||
do! Data.Page.add
|
||||
{ Page.empty with
|
||||
id = homePageId
|
||||
webLogId = webLogId
|
||||
authorId = userId
|
||||
title = "Welcome to myWebLog!"
|
||||
permalink = Permalink "welcome-to-myweblog.html"
|
||||
publishedOn = DateTime.UtcNow
|
||||
updatedOn = DateTime.UtcNow
|
||||
text = "<p>This is your default home page.</p>"
|
||||
revisions = [
|
||||
{ asOf = DateTime.UtcNow
|
||||
text = Html "<p>This is your default home page.</p>"
|
||||
}
|
||||
]
|
||||
} conn
|
||||
|
||||
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
||||
}
|
||||
|
||||
/// Create a new web log
|
||||
let create args sp = task {
|
||||
match args |> Array.length with
|
||||
| 5 -> return! createWebLog args sp
|
||||
| _ ->
|
||||
printfn "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
||||
return! System.Threading.Tasks.Task.CompletedTask
|
||||
}
|
||||
|
||||
/// Import prior permalinks from a text files with lines in the format "[old] [new]"
|
||||
let importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
|
||||
let conn = sp.GetRequiredService<IConnection> ()
|
||||
|
||||
match! Data.WebLog.findByHost urlBase conn with
|
||||
| Some webLog ->
|
||||
|
||||
let mapping =
|
||||
File.ReadAllLines file
|
||||
|> Seq.ofArray
|
||||
|> Seq.map (fun it ->
|
||||
let parts = it.Split " "
|
||||
Permalink parts[0], Permalink parts[1])
|
||||
|
||||
for old, current in mapping do
|
||||
match! Data.Post.findByPermalink current webLog.id conn with
|
||||
| Some post ->
|
||||
let! withLinks = rethink<Post> {
|
||||
withTable Data.Table.Post
|
||||
get post.id
|
||||
result conn
|
||||
}
|
||||
do! rethink {
|
||||
withTable Data.Table.Post
|
||||
get post.id
|
||||
update [ "priorPermalinks", old :: withLinks.priorPermalinks :> obj]
|
||||
write; ignoreResult conn
|
||||
}
|
||||
printfn $"{Permalink.toString old} -> {Permalink.toString current}"
|
||||
| None -> printfn $"Cannot find current post for {Permalink.toString current}"
|
||||
printfn "Done!"
|
||||
| None -> printfn $"No web log found at {urlBase}"
|
||||
}
|
||||
|
||||
/// Import permalinks if all is well
|
||||
let importPermalinks args sp = task {
|
||||
match args |> Array.length with
|
||||
| 3 -> return! importPriorPermalinks args[1] args[2] sp
|
||||
| _ ->
|
||||
printfn "Usage: MyWebLog import-permalinks [url] [file-name]"
|
||||
return! System.Threading.Tasks.Task.CompletedTask
|
||||
}
|
||||
|
||||
|
||||
open System.Collections.Generic
|
||||
open DotLiquid
|
||||
open DotLiquidBespoke
|
||||
open Giraffe
|
||||
open Giraffe.EndpointRouting
|
||||
open Microsoft.AspNetCore.Antiforgery
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.HttpOverrides
|
||||
open Microsoft.Extensions.Configuration
|
||||
open MyWebLog.ViewModels
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open RethinkDB.DistributedCache
|
||||
open RethinkDb.Driver.FSharp
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args =
|
||||
@ -210,36 +79,15 @@ let main args =
|
||||
let _ = builder.Services.AddGiraffe ()
|
||||
|
||||
// Set up DotLiquid
|
||||
[ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter>
|
||||
typeof<NavLinkFilter>; typeof<RelativeLinkFilter>; typeof<TagLinkFilter>; typeof<ThemeAssetFilter>
|
||||
typeof<ValueFilter>
|
||||
]
|
||||
|> List.iter Template.RegisterFilter
|
||||
|
||||
Template.RegisterTag<PageHeadTag> "page_head"
|
||||
Template.RegisterTag<UserLinksTag> "user_links"
|
||||
|
||||
[ // Domain types
|
||||
typeof<CustomFeed>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>; typeof<TagMap>; typeof<WebLog>
|
||||
// View models
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
||||
typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
||||
typeof<UserMessage>
|
||||
// Framework types
|
||||
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
||||
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||
]
|
||||
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
|
||||
DotLiquidBespoke.register ()
|
||||
|
||||
let app = builder.Build ()
|
||||
|
||||
match args |> Array.tryHead with
|
||||
| Some it when it = "init" ->
|
||||
NewWebLog.create args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||
Maintenance.createWebLog args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||
| Some it when it = "import-permalinks" ->
|
||||
NewWebLog.importPermalinks args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||
Maintenance.importPermalinks args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||
| _ ->
|
||||
let _ = app.UseForwardedHeaders ()
|
||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||
|
@ -3,7 +3,7 @@
|
||||
"hostname": "data02.bitbadger.solutions",
|
||||
"database": "myWebLog_dev"
|
||||
},
|
||||
"Generator": "myWebLog 2.0-alpha19",
|
||||
"Generator": "myWebLog 2.0-alpha20",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"MyWebLog.Handlers": "Debug"
|
||||
|
62
src/MyWebLog/themes/admin/layout-partial.liquid
Normal file
62
src/MyWebLog/themes/admin/layout-partial.liquid
Normal file
@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ page_title | escape }} « Admin « {{ web_log.name | escape }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ web_log.name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
{% if logged_on -%}
|
||||
<ul class="navbar-nav">
|
||||
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||
{{ "admin/pages" | nav_link: "Pages" }}
|
||||
{{ "admin/posts" | nav_link: "Posts" }}
|
||||
{{ "admin/categories" | nav_link: "Categories" }}
|
||||
{{ "admin/settings" | nav_link: "Settings" }}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
||||
{% if logged_on -%}
|
||||
{{ "admin/user/edit" | nav_link: "Edit User" }}
|
||||
{{ "user/log-off" | nav_link: "Log Off" }}
|
||||
{%- else -%}
|
||||
{{ "user/log-on" | nav_link: "Log On" }}
|
||||
{%- endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="mx-3">
|
||||
{% if messages %}
|
||||
<div class="messages mt-2">
|
||||
{% for msg in messages %}
|
||||
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
|
||||
{{ msg.message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
{% if msg.detail %}
|
||||
<hr>
|
||||
{{ msg.detail.value }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ content }}
|
||||
</main>
|
||||
<footer class="position-fixed bottom-0 w-100">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-end"><img src="/img/logo-light.png" alt="myWebLog" width="120" height="34"></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -8,11 +8,11 @@
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/themes/admin/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<body hx-boost="true">
|
||||
<header>
|
||||
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ "" | relative_link }}">{{ web_log.name }}</a>
|
||||
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ web_log.name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -20,7 +20,7 @@
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
{% if logged_on -%}
|
||||
<ul class="navbar-nav">
|
||||
{{ "admin" | nav_link: "Dashboard" }}
|
||||
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||
{{ "admin/pages" | nav_link: "Pages" }}
|
||||
{{ "admin/posts" | nav_link: "Posts" }}
|
||||
{{ "admin/categories" | nav_link: "Categories" }}
|
||||
@ -66,6 +66,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||
crossorigin="anonymous"></script>
|
||||
{{ htmx_script }}
|
||||
<script>
|
||||
const cssLoaded = [...document.styleSheets].filter(it => it.href.indexOf("bootstrap.min.css") > -1).length > 0
|
||||
if (!cssLoaded) {
|
||||
|
@ -27,7 +27,7 @@
|
||||
<label for="postsPerPage">Posts per Page</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-3 offset-xl-1 pb-3">
|
||||
<div class="col-12 col-md-4 col-xl-3 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="themePath" id="themePath" class="form-control" required>
|
||||
{% for theme in themes -%}
|
||||
@ -46,7 +46,7 @@
|
||||
<label for="timeZone">Time Zone</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-4 pb-3">
|
||||
<div class="col-12 col-md-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="defaultPage" id="defaultPage" class="form-control" required>
|
||||
{% for pg in pages -%}
|
||||
@ -59,6 +59,16 @@
|
||||
<label for="defaultPage">Default Page</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="autoHtmx" id="autoHtmx" class="form-check-input" value="true"
|
||||
{%- if model.auto_htmx %} checked="checked"{% endif %}>
|
||||
<label for="autoHtmx" class="form-check-label">Auto-Load htmx</label>
|
||||
</div>
|
||||
<span class="form-text fst-italic">
|
||||
<a href="https://htmx.org" target="_blank" rel="noopener">What is this?</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
|
@ -32,7 +32,7 @@
|
||||
<div>
|
||||
<small>
|
||||
{% if logged_on -%}
|
||||
<a href="{{ "admin" | relative_link }}">Dashboard</a> ~
|
||||
<a href="{{ "admin/dashboard" | relative_link }}">Dashboard</a> ~
|
||||
<a href="{{ "user/log-off" | relative_link }}">Log Off</a>
|
||||
{% else %}
|
||||
<a href="{{ "user/log-on" | relative_link }}">Log On</a>
|
||||
|
@ -139,7 +139,7 @@
|
||||
</a>
|
||||
• Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2">myWebLog</a> •
|
||||
{% if logged_on %}
|
||||
<a href="{{ "admin" | relative_link }}">Dashboard</a>
|
||||
<a href="{{ "admin/dashboard" | relative_link }}">Dashboard</a>
|
||||
{% else %}
|
||||
<a href="{{ "user/log-on" | relative_link }}">Log On</a>
|
||||
{%- endif %}
|
||||
|
14
src/MyWebLog/themes/tech-blog/layout-partial.liquid
Normal file
14
src/MyWebLog/themes/tech-blog/layout-partial.liquid
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>
|
||||
{%- if is_home -%}
|
||||
{{ web_log.name }}{% if web_log.subtitle %} | {{ web_log.subtitle.value }}{% endif %}
|
||||
{%- else -%}
|
||||
{{ page_title | strip_html }}{% if page_title and page_title != "" %} » {% endif %}{{ web_log.name }}
|
||||
{%- endif -%}
|
||||
</title>
|
||||
</head>
|
||||
<body>{{ content }}</body>
|
||||
</html>
|
@ -14,15 +14,21 @@
|
||||
{% page_head -%}
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<header class="site-header" id="top">
|
||||
<div class="header-logo">
|
||||
<a href="{{ "" | relative_link }}">
|
||||
<a href="{{ "" | relative_link }}" hx-boost="true" hx-target="#content" hx-swap="innerHTML show:#top:top"
|
||||
hx-indicator="#loadOverlay">
|
||||
<img src="{{ "img/bitbadger.png" | theme_asset }}"
|
||||
alt="A cartoon badger looking at a computer screen, with his paw on a mouse"
|
||||
title="Bit Badger Solutions">
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-title"><a href="{{ "" | relative_link }}">The Bit Badger Blog</a></div>
|
||||
<div class="header-title">
|
||||
<a href="{{ "" | relative_link }}" hx-boost="true" hx-target="#content" hx-swap="innerHTML show:#top:top"
|
||||
hx-indicator="#loadOverlay">
|
||||
The Bit Badger Blog
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-spacer"> </div>
|
||||
<div class="header-social">
|
||||
<a href="{{ "feed.xml" | relative_link }}" title="Subscribe to The Bit Badger Blog via RSS">
|
||||
@ -36,8 +42,12 @@
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content-wrapper">
|
||||
<main class="content" role="main">
|
||||
<div class="content-wrapper" hx-boost="true" hx-target="#content" hx-swap="innerHTML show:#top:top"
|
||||
hx-indicator="#loadOverlay">
|
||||
<div class="load-overlay" id="loadOverlay">
|
||||
<h1>Loading…</h1>
|
||||
</div>
|
||||
<main class="content" id="content" role="main">
|
||||
{{ content }}
|
||||
</main>
|
||||
<aside class="blog-sidebar">
|
||||
@ -77,5 +87,6 @@
|
||||
{%- endif %}
|
||||
</span>
|
||||
</footer>
|
||||
{% page_foot %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -204,6 +204,64 @@ li {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
.load-overlay {
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 90%;
|
||||
height: 0;
|
||||
z-index: 2000;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
border-radius: 1rem;
|
||||
animation: fadeOut .25s ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.load-overlay h1 {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, .75);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 1rem;
|
||||
width: 50%;
|
||||
padding: 1rem;
|
||||
}
|
||||
.load-overlay.htmx-request {
|
||||
display: block;
|
||||
height: 80vh;
|
||||
animation: fadeIn .25s ease-in-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
height: 80vh;
|
||||
}
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
height: 80vh;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
height: 80vh;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
@media all and (min-width: 68rem) {
|
||||
.content {
|
||||
width: 66rem;
|
||||
}
|
||||
.load-overlay {
|
||||
width: 60rem;
|
||||
left: 2rem;
|
||||
}
|
||||
}
|
||||
.home-title {
|
||||
text-align: left;
|
||||
line-height: 2rem;
|
||||
|
Loading…
Reference in New Issue
Block a user