Add metadata support
- Begin WIP on Bit Badger theme
|
@ -342,8 +342,10 @@ module Page =
|
||||||
"permalink", page.permalink
|
"permalink", page.permalink
|
||||||
"updatedOn", page.updatedOn
|
"updatedOn", page.updatedOn
|
||||||
"showInPageList", page.showInPageList
|
"showInPageList", page.showInPageList
|
||||||
|
"template", page.template
|
||||||
"text", page.text
|
"text", page.text
|
||||||
"priorPermalinks", page.priorPermalinks
|
"priorPermalinks", page.priorPermalinks
|
||||||
|
"metadata", page.metadata
|
||||||
"revisions", page.revisions
|
"revisions", page.revisions
|
||||||
]
|
]
|
||||||
write; withRetryDefault; ignoreResult
|
write; withRetryDefault; ignoreResult
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
namespace MyWebLog
|
namespace MyWebLog
|
||||||
|
|
||||||
open System
|
open System
|
||||||
|
open MyWebLog
|
||||||
|
|
||||||
/// A category under which a post may be identified
|
/// A category under which a post may be identified
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
@ -119,6 +120,9 @@ type Page =
|
||||||
/// The current text of the page
|
/// The current text of the page
|
||||||
text : string
|
text : string
|
||||||
|
|
||||||
|
/// Metadata for this page
|
||||||
|
metadata : MetaItem list
|
||||||
|
|
||||||
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
||||||
priorPermalinks : Permalink list
|
priorPermalinks : Permalink list
|
||||||
|
|
||||||
|
@ -141,6 +145,7 @@ module Page =
|
||||||
showInPageList = false
|
showInPageList = false
|
||||||
template = None
|
template = None
|
||||||
text = ""
|
text = ""
|
||||||
|
metadata = []
|
||||||
priorPermalinks = []
|
priorPermalinks = []
|
||||||
revisions = []
|
revisions = []
|
||||||
}
|
}
|
||||||
|
@ -182,6 +187,9 @@ type Post =
|
||||||
/// The tags for the post
|
/// The tags for the post
|
||||||
tags : string list
|
tags : string list
|
||||||
|
|
||||||
|
/// Metadata for the post
|
||||||
|
metadata : MetaItem list
|
||||||
|
|
||||||
/// Permalinks at which this post may have been previously served (useful for migrated content)
|
/// Permalinks at which this post may have been previously served (useful for migrated content)
|
||||||
priorPermalinks : Permalink list
|
priorPermalinks : Permalink list
|
||||||
|
|
||||||
|
@ -205,6 +213,7 @@ module Post =
|
||||||
text = ""
|
text = ""
|
||||||
categoryIds = []
|
categoryIds = []
|
||||||
tags = []
|
tags = []
|
||||||
|
metadata = []
|
||||||
priorPermalinks = []
|
priorPermalinks = []
|
||||||
revisions = []
|
revisions = []
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,13 @@ type MetaItem =
|
||||||
value : string
|
value : string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Functions to support metadata items
|
||||||
|
module MetaItem =
|
||||||
|
|
||||||
|
/// An empty metadata item
|
||||||
|
let empty =
|
||||||
|
{ name = ""; value = "" }
|
||||||
|
|
||||||
|
|
||||||
/// A revision of a page or post
|
/// A revision of a page or post
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
namespace MyWebLog.ViewModels
|
namespace MyWebLog.ViewModels
|
||||||
|
|
||||||
open System
|
open System
|
||||||
open System.Collections.Generic
|
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
|
|
||||||
/// Details about a category, used to display category lists
|
/// Details about a category, used to display category lists
|
||||||
|
@ -136,13 +135,21 @@ type EditPageModel =
|
||||||
|
|
||||||
/// The text of the page
|
/// The text of the page
|
||||||
text : string
|
text : string
|
||||||
|
|
||||||
|
/// Names of metadata items
|
||||||
|
metaNames : string[]
|
||||||
|
|
||||||
|
/// Values of metadata items
|
||||||
|
metaValues : string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an edit model from an existing page
|
/// Create an edit model from an existing page
|
||||||
static member fromPage (page : Page) =
|
static member fromPage (page : Page) =
|
||||||
let latest =
|
let latest =
|
||||||
match page.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
match page.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
||||||
| Some rev -> rev
|
| Some rev -> rev
|
||||||
| None -> Revision.empty
|
| None -> Revision.empty
|
||||||
|
let page = if page.metadata |> List.isEmpty then { page with metadata = [ MetaItem.empty ] } else page
|
||||||
{ pageId = PageId.toString page.id
|
{ pageId = PageId.toString page.id
|
||||||
title = page.title
|
title = page.title
|
||||||
permalink = Permalink.toString page.permalink
|
permalink = Permalink.toString page.permalink
|
||||||
|
@ -150,6 +157,8 @@ type EditPageModel =
|
||||||
isShownInPageList = page.showInPageList
|
isShownInPageList = page.showInPageList
|
||||||
source = MarkupText.sourceType latest.text
|
source = MarkupText.sourceType latest.text
|
||||||
text = MarkupText.text latest.text
|
text = MarkupText.text latest.text
|
||||||
|
metaNames = page.metadata |> List.map (fun m -> m.name) |> Array.ofList
|
||||||
|
metaValues = page.metadata |> List.map (fun m -> m.value) |> Array.ofList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,6 +191,12 @@ type EditPostModel =
|
||||||
|
|
||||||
/// Whether this post should be published
|
/// Whether this post should be published
|
||||||
doPublish : bool
|
doPublish : bool
|
||||||
|
|
||||||
|
/// Names of metadata items
|
||||||
|
metaNames : string[]
|
||||||
|
|
||||||
|
/// Values of metadata items
|
||||||
|
metaValues : string[]
|
||||||
}
|
}
|
||||||
/// Create an edit model from an existing past
|
/// Create an edit model from an existing past
|
||||||
static member fromPost (post : Post) =
|
static member fromPost (post : Post) =
|
||||||
|
@ -189,15 +204,18 @@ type EditPostModel =
|
||||||
match post.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
match post.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
||||||
| Some rev -> rev
|
| Some rev -> rev
|
||||||
| None -> Revision.empty
|
| None -> Revision.empty
|
||||||
{ postId = PostId.toString post.id
|
let post = if post.metadata |> List.isEmpty then { post with metadata = [ MetaItem.empty ] } else post
|
||||||
title = post.title
|
{ postId = PostId.toString post.id
|
||||||
permalink = Permalink.toString post.permalink
|
title = post.title
|
||||||
source = MarkupText.sourceType latest.text
|
permalink = Permalink.toString post.permalink
|
||||||
text = MarkupText.text latest.text
|
source = MarkupText.sourceType latest.text
|
||||||
tags = String.Join (", ", post.tags)
|
text = MarkupText.text latest.text
|
||||||
categoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList
|
tags = String.Join (", ", post.tags)
|
||||||
status = PostStatus.toString post.status
|
categoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList
|
||||||
doPublish = false
|
status = PostStatus.toString post.status
|
||||||
|
doPublish = false
|
||||||
|
metaNames = post.metadata |> List.map (fun m -> m.name) |> Array.ofList
|
||||||
|
metaValues = post.metadata |> List.map (fun m -> m.value) |> Array.ofList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -251,6 +269,9 @@ type PostListItem =
|
||||||
|
|
||||||
/// Tags for the post
|
/// Tags for the post
|
||||||
tags : string list
|
tags : string list
|
||||||
|
|
||||||
|
/// Metadata for the post
|
||||||
|
meta : MetaItem list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a post list item from a post
|
/// Create a post list item from a post
|
||||||
|
@ -265,6 +286,7 @@ type PostListItem =
|
||||||
text = post.text
|
text = post.text
|
||||||
categoryIds = post.categoryIds |> List.map CategoryId.toString
|
categoryIds = post.categoryIds |> List.map CategoryId.toString
|
||||||
tags = post.tags
|
tags = post.tags
|
||||||
|
meta = post.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -361,10 +361,13 @@ module Page =
|
||||||
}
|
}
|
||||||
match result with
|
match result with
|
||||||
| Some (title, page) ->
|
| Some (title, page) ->
|
||||||
|
let model = EditPageModel.fromPage page
|
||||||
return!
|
return!
|
||||||
Hash.FromAnonymousObject {|
|
Hash.FromAnonymousObject {|
|
||||||
csrf = csrfToken ctx
|
csrf = csrfToken ctx
|
||||||
model = EditPageModel.fromPage page
|
model = model
|
||||||
|
metadata = Array.zip model.metaNames model.metaValues
|
||||||
|
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
|
||||||
page_title = title
|
page_title = title
|
||||||
templates = templatesForTheme ctx "page"
|
templates = templatesForTheme ctx "page"
|
||||||
|}
|
|}
|
||||||
|
@ -408,6 +411,10 @@ module Page =
|
||||||
showInPageList = model.isShownInPageList
|
showInPageList = model.isShownInPageList
|
||||||
template = match model.template with "" -> None | tmpl -> Some tmpl
|
template = match model.template with "" -> None | tmpl -> Some tmpl
|
||||||
text = MarkupText.toHtml revision.text
|
text = MarkupText.toHtml revision.text
|
||||||
|
metadata = Seq.zip model.metaNames model.metaValues
|
||||||
|
|> Seq.filter (fun it -> fst it > "")
|
||||||
|
|> Seq.map (fun it -> { name = fst it; value = snd it })
|
||||||
|
|> List.ofSeq
|
||||||
revisions = revision :: page.revisions
|
revisions = revision :: page.revisions
|
||||||
}
|
}
|
||||||
do! (match model.pageId with "new" -> Data.Page.add | _ -> Data.Page.update) page conn
|
do! (match model.pageId with "new" -> Data.Page.add | _ -> Data.Page.update) page conn
|
||||||
|
|
|
@ -25,8 +25,8 @@ type WebLogMiddleware (next : RequestDelegate) =
|
||||||
/// DotLiquid filters
|
/// DotLiquid filters
|
||||||
module DotLiquidBespoke =
|
module DotLiquidBespoke =
|
||||||
|
|
||||||
open DotLiquid
|
|
||||||
open System.IO
|
open System.IO
|
||||||
|
open DotLiquid
|
||||||
|
|
||||||
/// A filter to generate nav links, highlighting the active link (exact match)
|
/// A filter to generate nav links, highlighting the active link (exact match)
|
||||||
type NavLinkFilter () =
|
type NavLinkFilter () =
|
||||||
|
@ -166,7 +166,7 @@ let main args =
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
.AddCookie(fun opts ->
|
.AddCookie(fun opts ->
|
||||||
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 20.
|
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 60.
|
||||||
opts.SlidingExpiration <- true
|
opts.SlidingExpiration <- true
|
||||||
opts.AccessDeniedPath <- "/forbidden")
|
opts.AccessDeniedPath <- "/forbidden")
|
||||||
let _ = builder.Services.AddLogging ()
|
let _ = builder.Services.AddLogging ()
|
||||||
|
|
|
@ -56,6 +56,50 @@
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
Metadata
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#metaItemContainer">
|
||||||
|
show
|
||||||
|
</button>
|
||||||
|
</legend>
|
||||||
|
<div id="metaItemContainer" class="collapse">
|
||||||
|
<div id="metaItems" class="container">
|
||||||
|
{%- for meta in metadata %}
|
||||||
|
<div id="meta_{{ meta[0] }}" class="row mb-3">
|
||||||
|
<div class="col-1 text-center align-self-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="Admin.removeMetaItem({{ meta[0] }})">
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" name="metaNames" id="metaNames_{{ meta[0] }}" class="form-control"
|
||||||
|
placeholder="Name" value="{{ meta[1] }}">
|
||||||
|
<label for="metaNames_{{ meta[0] }}">Name</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" name="metaValues" id="metaValues_{{ meta[0] }}" class="form-control"
|
||||||
|
placeholder="Value" value="{{ meta[2] }}">
|
||||||
|
<label for="metaValues_{{ meta[0] }}">Value</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor -%}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" onclick="Admin.addMetaItem()">Add an Item</button>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => Admin.setNextMetaIndex({{ metadata | size }}))
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|
98
src/MyWebLog/themes/bit-badger/home-page.liquid
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<div class="home">
|
||||||
|
<article class="content auto">
|
||||||
|
{{ page.text }}
|
||||||
|
</article>
|
||||||
|
<aside class="app-sidebar">
|
||||||
|
<div>
|
||||||
|
<div class="app-sidebar-head">Web Sites and Applications</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>PrayerTracker</strong><br>
|
||||||
|
<a href="/solutions/prayer-tracker" title="About PrayerTracker • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="https://prayer.bitbadger.solutions" title="PrayerTracker" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">
|
||||||
|
A prayer request tracking website (Free for any church or Sunday School class!)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>myPrayerJournal</strong><br>
|
||||||
|
<a href="/solutions/my-prayer-journal" title="About myPrayerJournal • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="https://prayerjournal.me" title="myPrayerJournal" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Minimalist personal prayer journal</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>Linux Resources</strong><br>
|
||||||
|
<a href="https://blog.bitbadger.solutions/linux/" title="Linux Resources" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Handy information for Linux folks</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="app-sidebar-head">WordPress</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>Futility Closet</strong><br>
|
||||||
|
<a href="/solutions/futility-closet" title="About Futility Closet • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="https://www.futilitycloset.com" title="Futility Closet" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">An idler’s miscellany of compendious amusements</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>Mindy Mackenzie</strong><br>
|
||||||
|
<a href="/solutions/mindy-mackenzie" title="About Mindy Mackenzie • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="https://mindymackenzie.com" title="Mindy Mackenzie" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>Riehl World News</strong><br>
|
||||||
|
<a href="/solutions/riehl-world-news" title="About Riehl World News • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="http://riehlworldview.com" title="Riehl World News" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Riehl news for real people</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="app-sidebar-head">Static Sites</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>Bay Vista Baptist Church</strong><br>
|
||||||
|
<a href="/solutions/bay-vista" title="About Bay Vista Baptist Church • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="https://bayvista.org" title="Bay Vista Baptist Church" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Biloxi, Mississippi</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>The Bit Badger Blog</strong><br>
|
||||||
|
<a href="/solutions/tech-blog" title="About The Bit Badger Blog • Bit Badger Solutions">About</a> •
|
||||||
|
<a href="https://blog.bitbadger.solutions" title="The Bit Badger Blog" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="app-sidebar-head">Personal</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>Daniel J. Summers</strong><br>
|
||||||
|
<a href="https://daniel.summershome.org" title="Daniel J. Summers" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Daniel’s personal blog</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="app-sidebar-name">
|
||||||
|
<strong>A Word from the Word</strong><br>
|
||||||
|
<a href="https://devotions.summershome.org" title="A Word from the Word" target="_blank">Visit</a>
|
||||||
|
</p>
|
||||||
|
<p class="app-sidebar-description">Devotions by Daniel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
45
src/MyWebLog/themes/bit-badger/layout.liquid
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>{{ page_title }} » Bit Badger Solutions</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald|Raleway">
|
||||||
|
<link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="header-logo">
|
||||||
|
<a href="/">
|
||||||
|
<img src="/themes/{{ web_log.theme_path }}/bitbadger.png"
|
||||||
|
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="/">Bit Badger Solutions</a></div>
|
||||||
|
<div class="header-spacer"> </div>
|
||||||
|
<div class="header-social">
|
||||||
|
<a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter" target="_blank">
|
||||||
|
<img src="/themes/{{ web_log.theme_path }}/twitter.png" alt="Twitter">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.facebook.com/bitbadger.solutions" title="Bit Badger Solutions on Facebook" target="_blank">
|
||||||
|
<img src="/themes/{{ web_log.theme_path }}/facebook.png" alt="Facebook">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{{ content }}
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<small>
|
||||||
|
{% if logged_on -%}
|
||||||
|
<a href="/admin">Dashboard</a> ~ <a href="/user/log-off">Log Off</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/user/log-on">Log On</a>
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
A <strong><a href="/">Bit Badger Solutions</a></strong> original design
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
5
src/MyWebLog/themes/bit-badger/single-page.liquid
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<article class="content auto">
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{{ page.text }}
|
||||||
|
<p><br><a href="/" title="Home">« Home</a></p>
|
||||||
|
</article>
|
|
@ -1,4 +1,93 @@
|
||||||
const Admin = {
|
const Admin = {
|
||||||
|
/** The next index for a metadata item */
|
||||||
|
nextMetaIndex : 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the next meta item index
|
||||||
|
* @param idx The index to set
|
||||||
|
*/
|
||||||
|
// Calling a function with a Liquid variable does not look like an error in the IDE...
|
||||||
|
setNextMetaIndex(idx) {
|
||||||
|
this.nextMetaIndex = idx
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new row for metadata entry
|
||||||
|
*/
|
||||||
|
addMetaItem() {
|
||||||
|
// Remove button
|
||||||
|
const removeBtn = document.createElement("button")
|
||||||
|
removeBtn.type = "button"
|
||||||
|
removeBtn.className = "btn btn-sm btn-danger"
|
||||||
|
removeBtn.innerHTML = "−"
|
||||||
|
removeBtn.setAttribute("onclick", `Admin.removeMetaItem(${this.nextMetaIndex})`)
|
||||||
|
|
||||||
|
const removeCol = document.createElement("div")
|
||||||
|
removeCol.className = "col-1 text-center align-self-center"
|
||||||
|
removeCol.appendChild(removeBtn)
|
||||||
|
|
||||||
|
// Name
|
||||||
|
const nameField = document.createElement("input")
|
||||||
|
nameField.type = "text"
|
||||||
|
nameField.name = "metaNames"
|
||||||
|
nameField.id = `metaNames_${this.nextMetaIndex}`
|
||||||
|
nameField.className = "form-control"
|
||||||
|
nameField.placeholder = "Name"
|
||||||
|
|
||||||
|
const nameLabel = document.createElement("label")
|
||||||
|
nameLabel.htmlFor = nameField.id
|
||||||
|
nameLabel.innerText = nameField.placeholder
|
||||||
|
|
||||||
|
const nameFloat = document.createElement("div")
|
||||||
|
nameFloat.className = "form-floating"
|
||||||
|
nameFloat.appendChild(nameField)
|
||||||
|
nameFloat.appendChild(nameLabel)
|
||||||
|
|
||||||
|
const nameCol = document.createElement("div")
|
||||||
|
nameCol.className = "col-3"
|
||||||
|
nameCol.appendChild(nameFloat)
|
||||||
|
|
||||||
|
// Value
|
||||||
|
const valueField = document.createElement("input")
|
||||||
|
valueField.type = "text"
|
||||||
|
valueField.name = "metaValues"
|
||||||
|
valueField.id = `metaValues_${this.nextMetaIndex}`
|
||||||
|
valueField.className = "form-control"
|
||||||
|
valueField.placeholder = "Value"
|
||||||
|
|
||||||
|
const valueLabel = document.createElement("label")
|
||||||
|
valueLabel.htmlFor = valueField.id
|
||||||
|
valueLabel.innerText = valueField.placeholder
|
||||||
|
|
||||||
|
const valueFloat = document.createElement("div")
|
||||||
|
valueFloat.className = "form-floating"
|
||||||
|
valueFloat.appendChild(valueField)
|
||||||
|
valueFloat.appendChild(valueLabel)
|
||||||
|
|
||||||
|
const valueCol = document.createElement("div")
|
||||||
|
valueCol.className = "col-8"
|
||||||
|
valueCol.appendChild(valueFloat)
|
||||||
|
|
||||||
|
// Put it all together
|
||||||
|
const newRow = document.createElement("div")
|
||||||
|
newRow.className = "row mb-3"
|
||||||
|
newRow.id = `meta_${this.nextMetaIndex}`
|
||||||
|
newRow.appendChild(removeCol)
|
||||||
|
newRow.appendChild(nameCol)
|
||||||
|
newRow.appendChild(valueCol)
|
||||||
|
|
||||||
|
document.getElementById("metaItems").appendChild(newRow)
|
||||||
|
this.nextMetaIndex++
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a metadata item
|
||||||
|
* @param idx The index of the metadata item to remove
|
||||||
|
*/
|
||||||
|
removeMetaItem(idx) {
|
||||||
|
document.getElementById(`meta_${idx}`).remove()
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm and delete a category
|
* Confirm and delete a category
|
||||||
* @param id The ID of the category to be deleted
|
* @param id The ID of the category to be deleted
|
||||||
|
|
BIN
src/MyWebLog/wwwroot/themes/bit-badger/bit-badger-auth.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/bitbadger.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/facebook.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/favicon.ico
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/screenshots/bay-vista.png
Normal file
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 23 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/screenshots/nsx.png
Normal file
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 71 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/screenshots/tcms.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
src/MyWebLog/wwwroot/themes/bit-badger/screenshots/tech-blog.png
Normal file
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 44 KiB |
217
src/MyWebLog/wwwroot/themes/bit-badger/style.css
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
html {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Raleway", "Segoe UI", Ubuntu, Tahoma, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
|
||||||
|
background-color: #FFFAFA;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: navy;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
border-bottom: dotted 1px navy;
|
||||||
|
}
|
||||||
|
a img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
acronym {
|
||||||
|
border-bottom: dotted 1px black;
|
||||||
|
}
|
||||||
|
header, h1, h2, h3, footer a {
|
||||||
|
font-family: "Oswald", "Segoe UI", Ubuntu, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.4rem 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 1.2rem 0;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
h2, h3 {
|
||||||
|
border-bottom: solid 2px navy;
|
||||||
|
}
|
||||||
|
@media all and (min-width:40rem) {
|
||||||
|
h2, h3 {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.auto {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
@media all and (min-width: 68rem) {
|
||||||
|
.content {
|
||||||
|
width: 66rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hdr {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.strike {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.alignleft {
|
||||||
|
float: left;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
.app-info {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row-reverse wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
abbr[title] {
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: dotted 1px rgba(0, 0, 0, .5)
|
||||||
|
}
|
||||||
|
/* Page header */
|
||||||
|
.site-header {
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-image: linear-gradient(to bottom, lightgray, #FFFAFA);
|
||||||
|
}
|
||||||
|
.site-header a, .site-header a:visited {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.site-header a:hover {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header-spacer {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
.header-social {
|
||||||
|
padding: 25px .8rem 0 0;
|
||||||
|
}
|
||||||
|
.header-social img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
@media all and (max-width:40rem) {
|
||||||
|
.site-header {
|
||||||
|
height: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
.header-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Home page */
|
||||||
|
@media all and (min-width: 80rem) {
|
||||||
|
.home {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.home-lead {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.app-sidebar {
|
||||||
|
text-align: center;
|
||||||
|
border-top: dotted 1px lightgray;
|
||||||
|
padding-top: 1rem;
|
||||||
|
font-size: .9rem;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.app-sidebar > div {
|
||||||
|
width: 20rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
@media all and (min-width: 68rem) {
|
||||||
|
.app-sidebar {
|
||||||
|
width: 66rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 80rem) {
|
||||||
|
.app-sidebar {
|
||||||
|
width: 12rem;
|
||||||
|
border-top: none;
|
||||||
|
border-left: dotted 1px lightgray;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.app-sidebar > div {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-sidebar a {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.app-sidebar-head {
|
||||||
|
font-family: "Oswald", "Segoe UI", Ubuntu, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
color: maroon;
|
||||||
|
margin-bottom: .8rem;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-bottom: solid 2px lightgray;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.app-sidebar-name, .app-sidebar-description {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.app-sidebar-description {
|
||||||
|
font-style: italic;
|
||||||
|
color: #555555;
|
||||||
|
padding-bottom: .6rem;
|
||||||
|
}
|
||||||
|
/* All solution page */
|
||||||
|
.app-name {
|
||||||
|
font-family: "Oswald", "Segoe UI", Ubuntu, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: maroon;
|
||||||
|
}
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 15px 10px 15px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: black;
|
||||||
|
clear: both;
|
||||||
|
background-image: linear-gradient(to bottom, #FFFAFA, lightgray);
|
||||||
|
}
|
||||||
|
footer a:link, footer a:visited {
|
||||||
|
color: black;
|
||||||
|
}
|
BIN
src/MyWebLog/wwwroot/themes/bit-badger/twitter.png
Normal file
After Width: | Height: | Size: 10 KiB |