V2 #1

Merged
danieljsummers merged 102 commits from v2 into main 2022-06-23 00:35:12 +00:00
35 changed files with 558 additions and 13 deletions
Showing only changes of commit f1249440b1 - Show all commits

View File

@ -342,8 +342,10 @@ module Page =
"permalink", page.permalink
"updatedOn", page.updatedOn
"showInPageList", page.showInPageList
"template", page.template
"text", page.text
"priorPermalinks", page.priorPermalinks
"metadata", page.metadata
"revisions", page.revisions
]
write; withRetryDefault; ignoreResult

View File

@ -1,6 +1,7 @@
namespace MyWebLog
open System
open MyWebLog
/// A category under which a post may be identified
[<CLIMutable; NoComparison; NoEquality>]
@ -119,6 +120,9 @@ type Page =
/// The current text of the page
text : string
/// Metadata for this page
metadata : MetaItem list
/// Permalinks at which this page may have been previously served (useful for migrated content)
priorPermalinks : Permalink list
@ -141,6 +145,7 @@ module Page =
showInPageList = false
template = None
text = ""
metadata = []
priorPermalinks = []
revisions = []
}
@ -182,6 +187,9 @@ type Post =
/// The tags for the post
tags : string list
/// Metadata for the post
metadata : MetaItem list
/// Permalinks at which this post may have been previously served (useful for migrated content)
priorPermalinks : Permalink list
@ -205,6 +213,7 @@ module Post =
text = ""
categoryIds = []
tags = []
metadata = []
priorPermalinks = []
revisions = []
}

View File

@ -98,7 +98,14 @@ type MetaItem =
value : string
}
/// Functions to support metadata items
module MetaItem =
/// An empty metadata item
let empty =
{ name = ""; value = "" }
/// A revision of a page or post
[<CLIMutable; NoComparison; NoEquality>]
type Revision =

View File

@ -1,7 +1,6 @@
namespace MyWebLog.ViewModels
open System
open System.Collections.Generic
open MyWebLog
/// Details about a category, used to display category lists
@ -136,13 +135,21 @@ type EditPageModel =
/// The text of the page
text : string
/// Names of metadata items
metaNames : string[]
/// Values of metadata items
metaValues : string[]
}
/// Create an edit model from an existing page
static member fromPage (page : Page) =
let latest =
match page.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
| Some rev -> rev
| None -> Revision.empty
let page = if page.metadata |> List.isEmpty then { page with metadata = [ MetaItem.empty ] } else page
{ pageId = PageId.toString page.id
title = page.title
permalink = Permalink.toString page.permalink
@ -150,6 +157,8 @@ type EditPageModel =
isShownInPageList = page.showInPageList
source = MarkupText.sourceType 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
doPublish : bool
/// Names of metadata items
metaNames : string[]
/// Values of metadata items
metaValues : string[]
}
/// Create an edit model from an existing past
static member fromPost (post : Post) =
@ -189,15 +204,18 @@ type EditPostModel =
match post.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
| Some rev -> rev
| None -> Revision.empty
{ postId = PostId.toString post.id
title = post.title
permalink = Permalink.toString post.permalink
source = MarkupText.sourceType latest.text
text = MarkupText.text latest.text
tags = String.Join (", ", post.tags)
categoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList
status = PostStatus.toString post.status
doPublish = false
let post = if post.metadata |> List.isEmpty then { post with metadata = [ MetaItem.empty ] } else post
{ postId = PostId.toString post.id
title = post.title
permalink = Permalink.toString post.permalink
source = MarkupText.sourceType latest.text
text = MarkupText.text latest.text
tags = String.Join (", ", post.tags)
categoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList
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 : string list
/// Metadata for the post
meta : MetaItem list
}
/// Create a post list item from a post
@ -265,6 +286,7 @@ type PostListItem =
text = post.text
categoryIds = post.categoryIds |> List.map CategoryId.toString
tags = post.tags
meta = post.metadata
}

View File

@ -361,10 +361,13 @@ module Page =
}
match result with
| Some (title, page) ->
let model = EditPageModel.fromPage page
return!
Hash.FromAnonymousObject {|
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
templates = templatesForTheme ctx "page"
|}
@ -408,6 +411,10 @@ module Page =
showInPageList = model.isShownInPageList
template = match model.template with "" -> None | tmpl -> Some tmpl
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
}
do! (match model.pageId with "new" -> Data.Page.add | _ -> Data.Page.update) page conn

View File

@ -25,8 +25,8 @@ type WebLogMiddleware (next : RequestDelegate) =
/// DotLiquid filters
module DotLiquidBespoke =
open DotLiquid
open System.IO
open DotLiquid
/// A filter to generate nav links, highlighting the active link (exact match)
type NavLinkFilter () =
@ -166,7 +166,7 @@ let main args =
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(fun opts ->
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 20.
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 60.
opts.SlidingExpiration <- true
opts.AccessDeniedPath <- "/forbidden")
let _ = builder.Services.AddLogging ()

View File

@ -56,6 +56,50 @@
<button type="submit" class="btn btn-primary">Save Changes</button>
</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] }})">
&minus;
</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>
</form>
</article>

View 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 &bull; 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 &bull; 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 &bull; 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 idlers 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 &bull; 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 &bull; 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 &bull; 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 &bull; 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">Daniels 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>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width" />
<title>{{ page_title }} &raquo; 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"> &nbsp; </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> &nbsp;
<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>

View File

@ -0,0 +1,5 @@
<article class="content auto">
<h1>{{ page.title }}</h1>
{{ page.text }}
<p><br><a href="/" title="Home">&laquo; Home</a></p>
</article>

View File

@ -1,4 +1,93 @@
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 = "&minus;"
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
* @param id The ID of the category to be deleted

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB