Migrate bit-badger theme

- Add generator string
- Focus meta name when adding new item
- Sort meta items
This commit is contained in:
Daniel J. Summers 2022-04-25 18:22:47 -04:00
parent f1249440b1
commit dec45fddba
53 changed files with 224 additions and 1937 deletions

View File

@ -326,7 +326,7 @@ module Page =
withTable Table.Page
getAll [ webLogId ] (nameof webLogId)
without [ "priorPermalinks"; "revisions" ]
orderBy "title"
orderByFunc (fun row -> row.G("title").Downcase ())
skip ((pageNbr - 1) * 25)
limit 25
result; withRetryDefault

View File

@ -46,7 +46,26 @@ type DisplayPage =
/// Is this the default page?
isDefault : bool
/// The text of the page
text : string
/// The metadata for the page
metadata : MetaItem list
}
/// Create a minimal display page (no text or metadata) from a database page
static member fromPageMinimal webLog (page : Page) =
let pageId = PageId.toString page.id
{ id = pageId
title = page.title
permalink = Permalink.toString page.permalink
publishedOn = page.publishedOn
updatedOn = page.updatedOn
showInPageList = page.showInPageList
isDefault = pageId = webLog.defaultPage
text = ""
metadata = []
}
/// Create a display page from a database page
static member fromPage webLog (page : Page) =
let pageId = PageId.toString page.id
@ -57,6 +76,8 @@ type DisplayPage =
updatedOn = page.updatedOn
showInPageList = page.showInPageList
isDefault = pageId = webLog.defaultPage
text = page.text
metadata = page.metadata
}

View File

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<Content Remove="Themes\BitBadger\solutions.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Themes\BitBadger\solutions.json" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

View File

@ -1,29 +0,0 @@
@{
var data = await SolutionInfo.GetAll();
string[] cats = new[] { "Web Sites and Applications", "WordPress", "Static Sites", "Personal" };
IEnumerable<SolutionInfo> solutionsForCat(string cat) =>
data.Where(it => it.Category == cat && it.FrontPage.Display).OrderBy(it => it.FrontPage.Order ?? 99);
string aboutTitle(string title) => $"{title} | Bit Badger Solutions";
}
<aside class="app-sidebar">
@foreach (var cat in cats)
{
<div>
<div class="app-sidebar-head">@cat</div>
@foreach (var sln in solutionsForCat(cat))
{
<div>
<p class="app-sidebar-name">
<strong>@sln.Name</strong><br>
@if (sln.LinkToAboutPage)
{
<a href="~/solutions/@(sln.Slug).html" title="@aboutTitle(sln.Name)">About</a><text> &bull; </text>
}
@if (sln.LinkToSite) { <a href="@sln.Url" title="@sln.Name" target="_blank">Visit</a> }
</p>
<p class="app-sidebar-description">@Html.Raw(sln.FrontPage.Text ?? sln.Summary ?? "")</p>
</div>
}
</div>
}
</aside>

View File

@ -1,56 +0,0 @@
@model MyWebLogModel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="generator" content="myWebLog @ViewBag.Version">
<title>@ViewBag.Title &laquo; @Model.WebLog.Name</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald|Raleway">
<link rel="stylesheet" asp-theme="@Model.WebLog.ThemePath" />
<link rel="icon" asp-theme="@Model.WebLog.ThemePath" />
</head>
<body>
<header class="site-header">
<div class="header-logo">
<a href="/">
<img asp-theme="@Model.WebLog.ThemePath" src="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 asp-theme="@Model.WebLog.ThemePath" src="twitter.png" alt="Twitter" />
</a>&nbsp; &nbsp;<a href="https://www.facebook.com/bitbadger.solutions" title="Bit Badger Solutions on Facebook" _target="_blank">
<img asp-theme="@Model.WebLog.ThemePath" src="facebook.png" alt="Facebook" />
</a>
</div>
</header>
<main>
@RenderBody()
</main>
<footer class="site-footer">
<p>
Powered by <strong><a href="#">myWebLog</a></strong> &bull;
@if (User is not null && (User.Identity?.IsAuthenticated ?? false))
{
<a href="/admin">@Resources.Dashboard</a>
}
else
{
<a href="/user/log-on">@Resources.LogOn</a>
}
</p>
<p>
A <strong><a href="/">Bit Badger Solutions</a></strong> original design
</p>
</footer>
</body>
</html>

View File

@ -1,13 +0,0 @@
@using MyWebLog.Features.Pages
@model SinglePageModel
@{
Layout = "_Layout";
ViewBag.Title = Model.Page.Title;
}
<div class="@(Model.IsHome ? "home" : null)">
<article class="content auto">
@if (!Model.IsHome) { <h2>@Model.Page.Title</h2> }
@Html.Raw(Model.Page.Text)
</article>
@if (Model.IsHome) { @await Html.PartialAsync("_AppSidebar") }
</div>

View File

@ -1,147 +0,0 @@
using System.Reflection;
using System.Text.Json;
namespace MyWebLog.Themes.BitBadger;
/// <summary>
/// A technology used in a solution
/// </summary>
public class Technology
{
/// <summary>
/// The name of the technology
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Why this technology was used in this project
/// </summary>
public string Purpose { get; set; } = "";
/// <summary>
/// Whether this project currently uses this technology
/// </summary>
public bool? IsCurrent { get; set; } = null;
}
/// <summary>
/// Information about the solutions displayed on the front page
/// </summary>
public class FrontPageInfo
{
/// <summary>
/// Whether the solution should be on the front page sidebar
/// </summary>
public bool Display { get; set; } = false;
/// <summary>
/// The order in which this solution should be displayed
/// </summary>
public byte? Order { get; set; } = null;
/// <summary>
/// The description text for the front page sidebar
/// </summary>
public string? Text { get; set; } = null;
}
/// <summary>
/// Information about a solution
/// </summary>
public class SolutionInfo
{
/// <summary>
/// The name of the solution
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// The URL slug for the page for this solution
/// </summary>
public string Slug { get; set; } = "";
/// <summary>
/// The URL for the solution (not the page describing it)
/// </summary>
public string Url { get; set; } = "";
/// <summary>
/// The category into which this solution falls
/// </summary>
public string Category { get; set; } = "";
/// <summary>
/// A short summary of the solution
/// </summary>
public string? Summary { get; set; } = null;
/// <summary>
/// Whether this solution is inactive
/// </summary>
public bool? IsInactive { get; set; } = null;
/// <summary>
/// Whether this solution is active
/// </summary>
public bool IsActive => !(IsInactive ?? false);
/// <summary>
/// Whether a link should not be generated to the URL for this solution
/// </summary>
public bool? DoNotLink { get; set; } = null;
/// <summary>
/// Whether a link should be generated to this solution
/// </summary>
public bool LinkToSite => !(DoNotLink ?? false);
/// <summary>
/// Whether an "About" link should be generated for this solution
/// </summary>
public bool? SkipAboutLink { get; set; } = null;
/// <summary>
/// Whether an "About" link should be generated for this solution
/// </summary>
public bool LinkToAboutPage => !(SkipAboutLink ?? false);
/// <summary>
/// Whether to generate a link to an archive site
/// </summary>
public bool? LinkToArchive { get; set; } = null;
/// <summary>
/// The URL of the archive site for this solution
/// </summary>
public string? ArchiveUrl { get; set; } = null;
/// <summary>
/// Home page sidebar display information
/// </summary>
public FrontPageInfo FrontPage { get; set; } = default!;
/// <summary>
/// Technologies used for this solution
/// </summary>
public ICollection<Technology> Technologies { get; set; } = new List<Technology>();
/// <summary>
/// Cache for reading solution info
/// </summary>
private static readonly Lazy<ValueTask<List<SolutionInfo>?>> _slnInfo = new(() =>
{
var asm = Assembly.GetAssembly(typeof(SolutionInfo))
?? throw new ArgumentNullException("Could not load the containing assembly");
using var stream = asm.GetManifestResourceStream("MyWebLog.Themes.BitBadger.solutions.json")
?? throw new ArgumentNullException("Could not load the solution data");
return JsonSerializer.DeserializeAsync<List<SolutionInfo>>(stream);
});
/// <summary>
/// Get all known solutions
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentNullException">if any required object is null</exception>
public static async Task<ICollection<SolutionInfo>> GetAll() =>
await _slnInfo.Value ?? throw new ArgumentNullException("Could not deserialize solution data");
}

View File

@ -1,43 +0,0 @@
@{
Layout = "_Layout";
ViewBag.Title = "All Solutions";
var data = await SolutionInfo.GetAll();
var active = data.Where(it => it.IsActive && it.LinkToAboutPage).OrderBy(it => it.Slug);
var inactive = data.Where(it => !it.IsActive && it.LinkToAboutPage).OrderBy(it => it.Slug);
}
<article class="content auto">
<h1>All Solutions</h1>
<h2>Active Solutions</h2>
@foreach (var sln in active)
{
<p>
<span class="app-name">@sln.Name</span> ~ <a href="~/solutions/@(sln.Slug).html">About</a>
@if (sln.IsActive)
{
<text>~ </text><a href="@sln.Url" target="_blank">Visit</a>
}
else if (sln.LinkToArchive ?? false)
{
<text>~ </text><a href="@sln.ArchiveUrl" target="_blank">Visit</a> <em>(archive)</em>
}
<br>@Html.Raw(sln.Summary)
</p>
}
<h2>Past Solutions</h2>
@foreach (var sln in inactive)
{
<p>
<span class="app-name">@sln.Name</span> ~ <a href="~/solutions/@(sln.Slug).html">About</a>
@if (sln.IsActive)
{
<text>~ </text><a href="@sln.Url" target="_blank">Visit</a>
}
else if (sln.LinkToArchive ?? false)
{
<text>~ </text><a href="@sln.ArchiveUrl" target="_blank">Visit</a> <em>(archive)</em>
}
<br>@Html.Raw(sln.Summary)
</p>
}
</article>

View File

@ -1,683 +0,0 @@
[
{
"Name": "A Word from the Word",
"Slug": "a-word-from-the-word",
"Url": "https://devotions.summershome.org",
"Category": "Personal",
"SkipAboutLink": true,
"FrontPage": {
"Display": true,
"Order": 2,
"Text": "Devotions by Daniel"
}
},
{
"Name": "Bay Vista Baptist Church",
"Slug": "bay-vista",
"Url": "https://bayvista.org",
"Category": "Static Sites",
"Summary": "Southern Baptist church in Biloxi, Mississippi",
"FrontPage": {
"Display": true,
"Order": 1,
"Text": "Biloxi, Mississippi"
},
"Technologies": [
{
"Name": "Hugo",
"Purpose": "static site generation",
"IsCurrent": true
},
{
"Name": "Azure",
"Purpose": "podcast file storage, automated builds, and static site hosting",
"IsCurrent": true
},
{
"Name": "GitHub",
"Purpose": "source code control",
"IsCurrent": true
},
{
"Name": "Hexo",
"Purpose": "static site generation"
},
{
"Name": "Jekyll",
"Purpose": "static site generation"
},
{
"Name": "WordPress",
"Purpose": "content management"
},
{
"Name": "MySQL",
"Purpose": "data storage"
}
]
},
{
"Name": "Cassy Fiano",
"Slug": "cassy-fiano",
"Url": "http://www.cassyfiano.com",
"Category": "WordPress",
"Summary": "A &ldquo;rising star&rdquo; conservative blogger",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging (with a custom theme)"
},
{
"Name": "MySQL",
"Purpose": "data storage"
},
{
"Name": "Rackspace Cloud",
"Purpose": "backup and recovery"
},
{
"Name": "Azure",
"Purpose": "backup and recovery"
}
]
},
{
"Name": "Daniel J. Summers",
"Slug": "daniel-j-summers",
"Url": "https://daniel.summershome.org",
"Category": "Personal",
"SkipAboutLink": true,
"FrontPage": {
"Display": true,
"Order": 1,
"Text": "Daniel&rsquo;s personal blog"
}
},
{
"Name": "Dr. Melissa Clouthier",
"Slug": "dr-melissa-clouthier",
"Url": "http://melissablogs.com",
"Category": "WordPress",
"Summary": "Politics, health, podcasts and more",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging (with a custom theme)"
},
{
"Name": "MySQL",
"Purpose": "data storage"
},
{
"Name": "Rackspace Cloud",
"Purpose": "backup and recovery"
},
{
"Name": "Azure",
"Purpose": "backup and recovery"
}
]
},
{
"Name": "Emerald Mountain Christian School",
"Slug": "emerald-mountain-christian-school",
"Url": "http://www.emeraldmountainchristianschool.org",
"Category": "Web Sites and Applications",
"Summary": "Classical, Christ-centered education near Wetumpka, Alabama",
"IsInactive": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "PHP",
"Purpose": "page generation and interactivity"
},
{
"Name": "ASP.NET MVC",
"Purpose": "page generation and interactivity"
},
{
"Name": "PostgreSQL",
"Purpose": "data storage"
},
{
"Name": "Rackspace Cloud",
"Purpose": "hosting"
},
{
"Name": "Azure",
"Purpose": "hosting"
}
]
},
{
"Name": "Futility Closet",
"Slug": "futility-closet",
"Url": "https://www.futilitycloset.com",
"Category": "WordPress",
"Summary": "An idler&rsquo;s miscellany of compendious amusements",
"FrontPage": {
"Display": true,
"Order": 1
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging",
"IsCurrent": true
},
{
"Name": "nginx",
"Purpose": "the web server",
"IsCurrent": true
},
{
"Name": "MySQL",
"Purpose": "data storage",
"IsCurrent": true
},
{
"Name": "Digital Ocean",
"Purpose": "web site hosting",
"IsCurrent": true
},
{
"Name": "Azure",
"Purpose": "backup and recovery",
"IsCurrent": true
},
{
"Name": "Rackspace Cloud",
"Purpose": "web site hosting"
}
]
},
{
"Name": "Hard Corps Wife",
"Slug": "hard-corps-wife",
"Url": "http://www.hardcorpswife.com",
"Category": "WordPress",
"Summary": "Cassy&rsquo;s life as a Marine wife",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging"
},
{
"Name": "MySQL",
"Purpose": "data storage"
},
{
"Name": "Rackspace Cloud",
"Purpose": "backup and recovery"
}
]
},
{
"Name": "Liberty Pundits",
"Slug": "liberty-pundits",
"Url": "http://libertypundits.net",
"Category": "WordPress",
"Summary": "The home for conservatives",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging"
},
{
"Name": "PHP",
"Purpose": "custom data migration software"
},
{
"Name": "MySQL",
"Purpose": "data storage"
}
]
},
{
"Name": "Linux Resources",
"Slug": "linux-resources",
"Url": "https://blog.bitbadger.solutions/linux/",
"Category": "Web Sites and Applications",
"SkipAboutLink": true,
"FrontPage": {
"Display": true,
"Order": 99,
"Text": "Handy information for Linux folks"
}
},
{
"Name": "Mindy Mackenzie",
"Slug": "mindy-mackenzie",
"Url": "https://mindymackenzie.com",
"Category": "WordPress",
"Summary": "<em>Wall Street Journal</em> best-selling author and C-suite advisor",
"FrontPage": {
"Display": true,
"Order": 2,
"Text": "WSJ-best-selling author of The Courage Solution"
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging and content management",
"IsCurrent": true
},
{
"Name": "nginx",
"Purpose": "the web server",
"IsCurrent": true
},
{
"Name": "MySQL",
"Purpose": "data storage",
"IsCurrent": true
},
{
"Name": "Digital Ocean",
"Purpose": "web site hosting",
"IsCurrent": true
},
{
"Name": "Azure",
"Purpose": "backup and recovery",
"IsCurrent": true
}
]
},
{
"Name": "myPrayerJournal",
"Slug": "my-prayer-journal",
"Url": "https://prayerjournal.me",
"Category": "Web Sites and Applications",
"Summary": "Minimalist personal prayer journal",
"FrontPage": {
"Display": true,
"Order": 2
},
"Technologies": [
{
"Name": "htmx",
"Purpose": "front end interactivity",
"IsCurrent": true
},
{
"Name": "Giraffe",
"Purpose": "the back end",
"IsCurrent": true
},
{
"Name": "LiteDB",
"Purpose": "data storage",
"IsCurrent": true
},
{
"Name": "GitHub",
"Purpose": "source code control",
"IsCurrent": true
},
{
"Name": "GitHub Pages",
"Purpose": "documentation",
"IsCurrent": true
},
{
"Name": "Vue.js",
"Purpose": "the front end"
},
{
"Name": "RavenDB",
"Purpose": "data storage"
},
{
"Name": "PostgreSQL",
"Purpose": "data storage"
}
]
},
{
"Name": "Not So Extreme Makeover: Community Edition",
"Slug": "nsx",
"Url": "http://notsoextreme.org",
"Category": "Web Sites and Applications",
"Summary": "Public site for the makeover; provides event-driven management of volunteers, donations, and families needing help",
"IsInactive": true,
"DoNotLink": true,
"LinkToArchive": true,
"ArchiveUrl": "https://nsx.archive.bitbadger.solutions",
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "content management"
},
{
"Name": "PHP",
"Purpose": "for NSXapp"
},
{
"Name": "MySQL",
"Purpose": "WordPress data storage"
},
{
"Name": "PostgreSQL",
"Purpose": "NSXapp data storage"
}
]
},
{
"Name": "Olivet Baptist Church",
"Slug": "olivet-baptist",
"Url": "https://olivet-baptist.org",
"Category": "Static Sites",
"Summary": "Southern Baptist church in Gulfport, Mississippi",
"IsInactive": true,
"DoNotLink": true,
"LinkToArchive": true,
"ArchiveUrl": "https://olivet.archive.bitbadger.solutions",
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "Azure",
"Purpose": "podcast file storage and archive site hosting",
"IsCurrent": true
},
{
"Name": "Vue.js",
"Purpose": "the user interface for the PWA"
},
{
"Name": "Hexo",
"Purpose": "for generating the site pages"
},
{
"Name": "WordPress",
"Purpose": "content management"
},
{
"Name": "MySQL",
"Purpose": "data storage"
}
]
},
{
"Name": "Photography by Michelle",
"Slug": "photography-by-michelle",
"Url": "https://www.summershome.org",
"Category": "Web Sites and Applications",
"Summary": "Photography services in Albuquerque, New Mexico",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "ASP.NET MVC",
"Purpose": "content management / gallery creation API"
},
{
"Name": "PostgreSQL",
"Purpose": "data storage"
},
{
"Name": "C# / Windows Forms",
"Purpose": "desktop gallery application"
},
{
"Name": "WordPress",
"Purpose": "content management"
},
{
"Name": "MySQL",
"Purpose": "data storage"
}
]
},
{
"Name": "PrayerTracker",
"Slug": "prayer-tracker",
"Url": "https://prayer.bitbadger.solutions",
"Category": "Web Sites and Applications",
"Summary": "Provides an ongoing, centralized prayer list for Sunday School classes and other groups",
"FrontPage": {
"Display": true,
"Order": 1,
"Text": "A prayer request tracking website (Free for any church or Sunday School class!)"
},
"Technologies": [
{
"Name": "Giraffe",
"Purpose": "server-side logic and dynamic page generation",
"IsCurrent": true
},
{
"Name": "PostgreSQL",
"Purpose": "data storage",
"IsCurrent": true
},
{
"Name": "GitHub",
"Purpose": "source code control",
"IsCurrent": true
},
{
"Name": "GitHub Pages",
"Purpose": "documentation hosting",
"IsCurrent": true
},
{
"Name": "MongoDB",
"Purpose": "data storage"
},
{
"Name": "ASP.NET MVC",
"Purpose": "dynamic content generation"
},
{
"Name": "Database Abstraction",
"Purpose": "data access"
},
{
"Name": "MySQL",
"Purpose": "data storage"
},
{
"Name": "PHP",
"Purpose": "dynamic content generation"
}
]
},
{
"Name": "Riehl World News",
"Slug": "riehl-world-news",
"Url": "http://riehlworldview.com",
"Category": "WordPress",
"Summary": "Riehl news for real people",
"FrontPage": {
"Display": true,
"Order": 3
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging",
"IsCurrent": true
},
{
"Name": "MySQL",
"Purpose": "data storage",
"IsCurrent": true
},
{
"Name": "Azure",
"Purpose": "backup and recovery",
"IsCurrent": true
},
{
"Name": "F#",
"Purpose": "custom archive static page generation"
}
]
},
{
"Name": "The Clearinghouse Management System",
"Slug": "tcms",
"Url": "http://tcms.us",
"Category": "Web Sites and Applications",
"Summary": "Assists a needs clearinghouse in connecting people with needs to people that can help meet those needs",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "PHP",
"Purpose": "the TCMS application logic"
},
{
"Name": "WordPress",
"Purpose": "publicly-facing pages and authentication"
},
{
"Name": "PostgreSQL",
"Purpose": "application data storage"
},
{
"Name": "MySQL",
"Purpose": "WordPress data storage"
}
]
},
{
"Name": "The Bit Badger Blog",
"Slug": "tech-blog",
"Url": "https://blog.bitbadger.solutions",
"Category": "Static Sites",
"Summary": "Geek stuff from Bit Badger Solutions",
"FrontPage": {
"Display": true,
"Order": 3,
"Text": "Technical information (&ldquo;geek stuff&rdquo;) from Bit Badger Solutions"
},
"Technologies": [
{
"Name": "Hexo",
"Purpose": "static site generation",
"IsCurrent": true
},
{
"Name": "Azure",
"Purpose": "static site hosting",
"IsCurrent": true
},
{
"Name": "GitHub",
"Purpose": "source code control",
"IsCurrent": true
},
{
"Name": "Custom Software",
"Purpose": "content management"
},
{
"Name": "WordPress",
"Purpose": "content management"
},
{
"Name": "BlogEngine.NET",
"Purpose": "content management"
},
{
"Name": "Orchard",
"Purpose": "content management"
},
{
"Name": "myWebLog",
"Purpose": "content management"
},
{
"Name": "Jekyll",
"Purpose": "static site generation"
},
{
"Name": "MySQL",
"Purpose": "data storage"
},
{
"Name": "SQL Sever",
"Purpose": "data storage"
},
{
"Name": "RethinkDB",
"Purpose": "data storage"
}
]
},
{
"Name": "The Shark Tank",
"Slug": "the-shark-tank",
"Url": "http://shark-tank.net",
"Category": "WordPress",
"Summary": "Florida&rsquo;s political feeding frenzy",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "WordPress",
"Purpose": "blogging"
}
]
},
{
"Name": "Virtual Prayer Room",
"Slug": "virtual-prayer-room",
"Url": "https://virtualprayerroom.us",
"Category": "Web Sites and Applications",
"Summary": "Gives prayer warriors access to requests from wherever they may be, and sends them daily updates",
"IsInactive": true,
"DoNotLink": true,
"FrontPage": {
"Display": false
},
"Technologies": [
{
"Name": "PHP",
"Purpose": "the application logic"
},
{
"Name": "PostgreSQL",
"Purpose": "data storage"
}
]
}
]

View File

@ -1,6 +0,0 @@
@namespace MyWebLog.Themes
@using MyWebLog.Features.Shared
@using MyWebLog.Properties
@addTagHelper *, MyWebLog

View File

@ -1,262 +0,0 @@
html {
background-color: lightgray;
}
body {
margin: 0px;
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)
}
/* Header Style */
.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;
}
.site-header .header-title {
font-size: 3rem;
font-weight: bold;
line-height: 100px;
text-align: center;
}
.site-header .header-spacer {
flex-grow: 3;
}
.site-header .header-social {
padding: 25px .8rem 0 0;
}
.site-header .header-social img {
width: 50px;
height: 50px;
}
@media all and (max-width:40rem) {
.site-header {
height: auto;
flex-direction: column;
align-items: center;
}
.site-header .header-title {
line-height: 3rem;
}
.site-header .header-spacer {
display: none;
}
}
/* Home Page Styles */
@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;
}
/* Home Page Sidebar Styles */
.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;
}
/* Solutions Page Styles */
.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 Styles */
footer.site-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.site-footer a:link, footer.site-footer a:visited {
color: black;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32210.238
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.Themes.BitBadger", "MyWebLog.Themes.BitBadger\MyWebLog.Themes.BitBadger.csproj", "{729F7AB3-2300-4390-B972-71D32FBBBF50}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}"
@ -17,10 +15,6 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Release|Any CPU.Build.0 = Release|Any CPU
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -1,490 +0,0 @@
namespace MyWebLog.Domain
// -- Supporting Types --
/// Types of markup text supported
type MarkupText =
/// Text in Markdown format
| Markdown of string
/// Text in HTML format
| Html of string
/// Functions to support maniuplating markup text
module MarkupText =
/// Get the string representation of this markup text
let toString it =
match it with
| Markdown x -> "Markdown", x
| Html x -> "HTML", x
||> sprintf "%s: %s"
/// Get the HTML value of the text
let toHtml = function
| Markdown it -> sprintf "TODO: convert to HTML - %s" it
| Html it -> it
/// Parse a string representation to markup text
let ofString (it : string) =
match true with
| _ when it.StartsWith "Markdown: " -> it.Substring 10 |> Markdown
| _ when it.StartsWith "HTML: " -> it.Substring 6 |> Html
| _ -> sprintf "Cannot determine text type - %s" it |> invalidOp
/// Authorization levels
type AuthorizationLevel =
/// Authorization to administer a weblog
| Administrator
/// Authorization to comment on a weblog
| User
/// Functions to support authorization levels
module AuthorizationLevel =
/// Get the string reprsentation of an authorization level
let toString = function Administrator -> "Administrator" | User -> "User"
/// Create an authorization level from a string
let ofString it =
match it with
| "Administrator" -> Administrator
| "User" -> User
| _ -> sprintf "%s is not an authorization level" it |> invalidOp
/// Post statuses
type PostStatus =
/// Post has not been released for public consumption
| Draft
/// Post is released
| Published
/// Functions to support post statuses
module PostStatus =
/// Get the string representation of a post status
let toString = function Draft -> "Draft" | Published -> "Published"
/// Create a post status from a string
let ofString it =
match it with
| "Draft" -> Draft
| "Published" -> Published
| _ -> sprintf "%s is not a post status" it |> invalidOp
/// Comment statuses
type CommentStatus =
/// Comment is approved
| Approved
/// Comment has yet to be approved
| Pending
/// Comment was flagged as spam
| Spam
/// Functions to support comment statuses
module CommentStatus =
/// Get the string representation of a comment status
let toString = function Approved -> "Approved" | Pending -> "Pending" | Spam -> "Spam"
/// Create a comment status from a string
let ofString it =
match it with
| "Approved" -> Approved
| "Pending" -> Pending
| "Spam" -> Spam
| _ -> sprintf "%s is not a comment status" it |> invalidOp
/// Seconds since the Unix epoch
type UnixSeconds = UnixSeconds of int64
/// Functions to support Unix seconds
module UnixSeconds =
/// Get the long (int64) representation of Unix seconds
let toLong = function UnixSeconds it -> it
/// Zero seconds past the epoch
let none = UnixSeconds 0L
// -- IDs --
open System
// See https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID for info on "short GUIDs"
/// A short GUID
type ShortGuid = ShortGuid of Guid
/// Functions to support short GUIDs
module ShortGuid =
/// Encode a GUID into a short GUID
let toString = function
| ShortGuid guid ->
Convert.ToBase64String(guid.ToByteArray ())
.Replace("/", "_")
.Replace("+", "-")
.Substring (0, 22)
/// Decode a short GUID into a GUID
let ofString (it : string) =
it.Replace("_", "/").Replace ("-", "+")
|> (sprintf "%s==" >> Convert.FromBase64String >> Guid >> ShortGuid)
/// Create a new short GUID
let create () = (Guid.NewGuid >> ShortGuid) ()
/// The empty short GUID
let empty = ShortGuid Guid.Empty
/// The ID of a category
type CategoryId = CategoryId of ShortGuid
/// Functions to support category IDs
module CategoryId =
/// Get the string representation of a page ID
let toString = function CategoryId it -> ShortGuid.toString it
/// Create a category ID from its string representation
let ofString = ShortGuid.ofString >> CategoryId
/// An empty category ID
let empty = CategoryId ShortGuid.empty
/// The ID of a comment
type CommentId = CommentId of ShortGuid
/// Functions to support comment IDs
module CommentId =
/// Get the string representation of a comment ID
let toString = function CommentId it -> ShortGuid.toString it
/// Create a comment ID from its string representation
let ofString = ShortGuid.ofString >> CommentId
/// An empty comment ID
let empty = CommentId ShortGuid.empty
/// The ID of a page
type PageId = PageId of ShortGuid
/// Functions to support page IDs
module PageId =
/// Get the string representation of a page ID
let toString = function PageId it -> ShortGuid.toString it
/// Create a page ID from its string representation
let ofString = ShortGuid.ofString >> PageId
/// An empty page ID
let empty = PageId ShortGuid.empty
/// The ID of a post
type PostId = PostId of ShortGuid
/// Functions to support post IDs
module PostId =
/// Get the string representation of a post ID
let toString = function PostId it -> ShortGuid.toString it
/// Create a post ID from its string representation
let ofString = ShortGuid.ofString >> PostId
/// An empty post ID
let empty = PostId ShortGuid.empty
/// The ID of a user
type UserId = UserId of ShortGuid
/// Functions to support user IDs
module UserId =
/// Get the string representation of a user ID
let toString = function UserId it -> ShortGuid.toString it
/// Create a user ID from its string representation
let ofString = ShortGuid.ofString >> UserId
/// An empty user ID
let empty = UserId ShortGuid.empty
/// The ID of a web log
type WebLogId = WebLogId of ShortGuid
/// Functions to support web log IDs
module WebLogId =
/// Get the string representation of a web log ID
let toString = function WebLogId it -> ShortGuid.toString it
/// Create a web log ID from its string representation
let ofString = ShortGuid.ofString >> WebLogId
/// An empty web log ID
let empty = WebLogId ShortGuid.empty
// -- Domain Entities --
// fsharplint:disable RecordFieldNames
/// A revision of a post or page
type Revision = {
/// The instant which this revision was saved
asOf : UnixSeconds
/// The text
text : MarkupText
}
with
/// An empty revision
static member empty =
{ asOf = UnixSeconds.none
text = Markdown ""
}
/// A page with static content
[<CLIMutable; NoComparison; NoEquality>]
type Page = {
/// The Id
id : PageId
/// The Id of the web log to which this page belongs
webLogId : WebLogId
/// The Id of the author of this page
authorId : UserId
/// The title of the page
title : string
/// The link at which this page is displayed
permalink : string
/// The instant this page was published
publishedOn : UnixSeconds
/// The instant this page was last updated
updatedOn : UnixSeconds
/// Whether this page shows as part of the web log's navigation
showInPageList : bool
/// The current text of the page
text : MarkupText
/// Revisions of this page
revisions : Revision list
}
with
static member empty =
{ id = PageId.empty
webLogId = WebLogId.empty
authorId = UserId.empty
title = ""
permalink = ""
publishedOn = UnixSeconds.none
updatedOn = UnixSeconds.none
showInPageList = false
text = Markdown ""
revisions = []
}
/// An entry in the list of pages displayed as part of the web log (derived via query)
type PageListEntry = {
/// The permanent link for the page
permalink : string
/// The title of the page
title : string
}
/// A web log
[<CLIMutable; NoComparison; NoEquality>]
type WebLog = {
/// The Id
id : WebLogId
/// The name
name : string
/// The subtitle
subtitle : string option
/// The default page ("posts" or a page Id)
defaultPage : string
/// The path of the theme (within /views/themes)
themePath : string
/// The URL base
urlBase : string
/// The time zone in which dates/times should be displayed
timeZone : string
/// A list of pages to be rendered as part of the site navigation (not stored)
pageList : PageListEntry list
}
with
/// An empty web log
static member empty =
{ id = WebLogId.empty
name = ""
subtitle = None
defaultPage = ""
themePath = "default"
urlBase = ""
timeZone = "America/New_York"
pageList = []
}
/// An authorization between a user and a web log
type Authorization = {
/// The Id of the web log to which this authorization grants access
webLogId : WebLogId
/// The level of access granted by this authorization
level : AuthorizationLevel
}
/// A user of myWebLog
[<CLIMutable; NoComparison; NoEquality>]
type User = {
/// The Id
id : UserId
/// The user name (e-mail address)
userName : string
/// The first name
firstName : string
/// The last name
lastName : string
/// The user's preferred name
preferredName : string
/// The hash of the user's password
passwordHash : string
/// The URL of the user's personal site
url : string option
/// The user's authorizations
authorizations : Authorization list
}
with
/// An empty user
static member empty =
{ id = UserId.empty
userName = ""
firstName = ""
lastName = ""
preferredName = ""
passwordHash = ""
url = None
authorizations = []
}
/// Functions supporting users
module User =
/// Claims for this user
let claims user =
user.authorizations
|> List.map (fun a -> sprintf "%s|%s" (WebLogId.toString a.webLogId) (AuthorizationLevel.toString a.level))
/// A category to which posts may be assigned
[<CLIMutable; NoComparison; NoEquality>]
type Category = {
/// The Id
id : CategoryId
/// The Id of the web log to which this category belongs
webLogId : WebLogId
/// The displayed name
name : string
/// The slug (used in category URLs)
slug : string
/// A longer description of the category
description : string option
/// The parent Id of this category (if a subcategory)
parentId : CategoryId option
/// The categories for which this category is the parent
children : CategoryId list
}
with
/// An empty category
static member empty =
{ id = CategoryId.empty
webLogId = WebLogId.empty
name = ""
slug = ""
description = None
parentId = None
children = []
}
/// A comment (applies to a post)
[<CLIMutable; NoComparison; NoEquality>]
type Comment = {
/// The Id
id : CommentId
/// The Id of the post to which this comment applies
postId : PostId
/// The Id of the comment to which this comment is a reply
inReplyToId : CommentId option
/// The name of the commentor
name : string
/// The e-mail address of the commentor
email : string
/// The URL of the commentor's personal website
url : string option
/// The status of the comment
status : CommentStatus
/// The instant the comment was posted
postedOn : UnixSeconds
/// The text of the comment
text : string
}
with
static member empty =
{ id = CommentId.empty
postId = PostId.empty
inReplyToId = None
name = ""
email = ""
url = None
status = Pending
postedOn = UnixSeconds.none
text = ""
}
/// A post
[<CLIMutable; NoComparison; NoEquality>]
type Post = {
/// The Id
id : PostId
/// The Id of the web log to which this post belongs
webLogId : WebLogId
/// The Id of the author of this post
authorId : UserId
/// The status
status : PostStatus
/// The title
title : string
/// The link at which the post resides
permalink : string
/// The instant on which the post was originally published
publishedOn : UnixSeconds
/// The instant on which the post was last updated
updatedOn : UnixSeconds
/// The text of the post
text : MarkupText
/// The Ids of the categories to which this is assigned
categoryIds : CategoryId list
/// The tags for the post
tags : string list
/// The permalinks at which this post may have once resided
priorPermalinks : string list
/// Revisions of this post
revisions : Revision list
/// The categories to which this is assigned (not stored in database)
categories : Category list
/// The comments (not stored in database)
comments : Comment list
}
with
static member empty =
{ id = PostId.empty
webLogId = WebLogId.empty
authorId = UserId.empty
status = Draft
title = ""
permalink = ""
publishedOn = UnixSeconds.none
updatedOn = UnixSeconds.none
text = Markdown ""
categoryIds = []
tags = []
priorPermalinks = []
revisions = []
categories = []
comments = []
}
// --- UI Support ---
/// Counts of items displayed on the admin dashboard
type DashboardCounts = {
/// The number of pages for the web log
pages : int
/// The number of pages for the web log
posts : int
/// The number of categories for the web log
categories : int
}

View File

@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Strings.fs" />
<Compile Include="Domain.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<Content Remove="Resources/en-US.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/en-US.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DotLiquid" Version="2.2.548" />
<PackageReference Include="RethinkDb.Driver" Version="2.3.150" />
<PackageReference Include="Suave" Version="2.6.1" />
<PackageReference Include="Suave.DotLiquid" Version="2.6.1" />
</ItemGroup>
</Project>

View File

@ -1,4 +0,0 @@
open MyWebLog
open Suave
startWebServer defaultConfig (Successful.OK (Strings.get "LastUpdated"))

View File

@ -1,83 +0,0 @@
{
"Action": "Action",
"Added": "Added",
"AddNew": "Add New",
"AddNewCategory": "Add New Category",
"AddNewPage": "Add New Page",
"AddNewPost": "Add New Post",
"Admin": "Admin",
"AndPublished": " and Published",
"andXMore": "and {0} more...",
"at": "at",
"BackToCategoryList": "Back to Category List",
"BackToPageList": "Back to Page List",
"BackToPostList": "Back to Post List",
"Categories": "Categories",
"Category": "Category",
"CategoryDeleteWarning": "Are you sure you wish to delete the category",
"Close": "Close",
"Comments": "Comments",
"Dashboard": "Dashboard",
"Date": "Date",
"Delete": "Delete",
"Description": "Description",
"Edit": "Edit",
"EditCategory": "Edit Category",
"EditPage": "Edit Page",
"EditPost": "Edit Post",
"EmailAddress": "E-mail Address",
"ErrBadAppConfig": "Could not convert config.json to myWebLog configuration",
"ErrBadLogOnAttempt": "Invalid e-mail address or password",
"ErrDataConfig": "Could not convert data-config.json to RethinkDB connection",
"ErrNotConfigured": "is not properly configured for myWebLog",
"Error": "Error",
"LastUpdated": "Last Updated",
"LastUpdatedDate": "Last Updated Date",
"ListAll": "List All",
"LoadedIn": "Loaded in",
"LogOff": "Log Off",
"LogOn": "Log On",
"MsgCategoryDeleted": "Deleted category {0} successfully",
"MsgCategoryEditSuccess": "{0} category successfully",
"MsgLogOffSuccess": "Log off successful | Have a nice day!",
"MsgLogOnSuccess": "Log on successful | Welcome to myWebLog!",
"MsgPageDeleted": "Deleted page successfully",
"MsgPageEditSuccess": "{0} page successfully",
"MsgPostEditSuccess": "{0}{1} post successfully",
"Name": "Name",
"NewerPosts": "Newer Posts",
"NextPost": "Next Post",
"NoComments": "No Comments",
"NoParent": "No Parent",
"OlderPosts": "Older Posts",
"OneComment": "1 Comment",
"PageDeleteWarning": "Are you sure you wish to delete the page",
"PageDetails": "Page Details",
"PageHash": "Page #",
"Pages": "Pages",
"ParentCategory": "Parent Category",
"Password": "Password",
"Permalink": "Permalink",
"PermanentLinkTo": "Permanent Link to",
"PostDetails": "Post Details",
"Posts": "Posts",
"PostsTagged": "Posts Tagged",
"PostStatus": "Post Status",
"PoweredBy": "Powered by",
"PreviousPost": "Previous Post",
"PublishedDate": "Published Date",
"PublishThisPost": "Publish This Post",
"Save": "Save",
"Seconds": "Seconds",
"ShowInPageList": "Show in Page List",
"Slug": "Slug",
"startingWith": "starting with",
"Status": "Status",
"Tags": "Tags",
"Time": "Time",
"Title": "Title",
"Updated": "Updated",
"View": "View",
"Warning": "Warning",
"XComments": "{0} Comments"
}

View File

@ -1,40 +0,0 @@
module MyWebLog.Strings
open System.Collections.Generic
open System.Globalization
open System.IO
open System.Reflection
open System.Text.Json
/// The locales we'll try to load
let private supportedLocales = [ "en-US" ]
/// The fallback locale, if a key is not found in a non-default locale
let private fallbackLocale = "en-US"
/// Get an embedded JSON file as a string
let private getEmbedded locale =
let str = sprintf "MyWebLog.Resources.%s.json" locale |> Assembly.GetExecutingAssembly().GetManifestResourceStream
use rdr = new StreamReader (str)
rdr.ReadToEnd()
/// The dictionary of localized strings
let private strings =
supportedLocales
|> List.map (fun loc -> loc, getEmbedded loc |> JsonSerializer.Deserialize<Dictionary<string, string>>)
|> dict
/// Get a key from the resources file for the given locale
let getForLocale locale key =
let getString thisLocale =
match strings.ContainsKey thisLocale && strings.[thisLocale].ContainsKey key with
| true -> Some strings.[thisLocale].[key]
| false -> None
match getString locale with
| Some xlat -> Some xlat
| None when locale <> fallbackLocale -> getString fallbackLocale
| None -> None
|> function Some xlat -> xlat | None -> sprintf "%s.%s" locale key
/// Translate the key for the current locale
let get key = getForLocale CultureInfo.CurrentCulture.Name key

View File

@ -58,6 +58,7 @@ open System.Collections.Generic
module private Helpers =
open Microsoft.AspNetCore.Antiforgery
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open System.Security.Claims
open System.IO
@ -93,7 +94,17 @@ module private Helpers =
return msg |> (List.rev >> Array.ofList)
| None -> return [||]
}
/// Hold variable for the configured generator string
let mutable private generatorString : string option = None
/// Get the generator string
let private generator (ctx : HttpContext) =
if Option.isNone generatorString then
let cfg = ctx.RequestServices.GetRequiredService<IConfiguration> ()
generatorString <- Option.ofObj cfg["Generator"]
match generatorString with Some gen -> gen | None -> "generator not configured"
/// Either get the web log from the hash, or get it from the cache and add it to the hash
let private deriveWebLogFromHash (hash : Hash) ctx =
match hash.ContainsKey "web_log" with
@ -112,6 +123,7 @@ module private Helpers =
hash.Add ("page_list", PageListCache.get ctx)
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
hash.Add ("messages", messages)
hash.Add ("generator", generator ctx)
do! commitSession ctx
@ -343,7 +355,7 @@ module Page =
let! pages = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx)
return!
Hash.FromAnonymousObject
{| pages = pages |> List.map (DisplayPage.fromPage webLog)
{| pages = pages |> List.map (DisplayPage.fromPageMinimal webLog)
page_title = "Pages"
|}
|> viewForTheme "admin" "page-list" next ctx
@ -414,6 +426,7 @@ module Page =
metadata = Seq.zip model.metaNames model.metaValues
|> Seq.filter (fun it -> fst it > "")
|> Seq.map (fun it -> { name = fst it; value = snd it })
|> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}")
|> List.ofSeq
revisions = revision :: page.revisions
}
@ -482,7 +495,7 @@ module Post =
match! Data.Page.findById (PageId pageId) webLog.id (conn ctx) with
| Some page ->
return!
Hash.FromAnonymousObject {| page = page; page_title = page.title |}
Hash.FromAnonymousObject {| page = DisplayPage.fromPage webLog page; page_title = page.title |}
|> themedView (defaultArg page.template "single-page") next ctx
| None -> return! Error.notFound next ctx
}
@ -501,7 +514,7 @@ module Post =
match! Data.Page.findByPermalink permalink webLog.id conn with
| Some page ->
return!
Hash.FromAnonymousObject {| page = page; page_title = page.title |}
Hash.FromAnonymousObject {| page = DisplayPage.fromPage webLog page; page_title = page.title |}
|> themedView (defaultArg page.template "single-page") next ctx
| None ->
// Prior post

View File

@ -8,5 +8,6 @@
"RethinkDB.DistributedCache": "Debug",
"RethinkDb.Driver": "Debug"
}
}
},
"Generator": "myWebLog 2.0-alpha01"
}

View File

@ -1,7 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width">
<meta name="generator" content="{{ generator }}">
<title>{{ page_title | escape }} &laquo; Admin &laquo; {{ web_log.name | escape }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

View File

@ -21,7 +21,7 @@
<div class="form-floating pb-3">
<select name="template" id="template" class="form-control">
{% for tmpl in templates -%}
<option value="{{ tmpl[0] }}"{% if model.template == tmpl %} selected="selected"{% endif %}>
<option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected="selected"{% endif %}>
{{ tmpl[1] }}
</option>
{%- endfor %}

View File

@ -5,6 +5,7 @@
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Permalink</th>
<th scope="col">Last Updated</th>
</tr>
</thead>
@ -23,6 +24,7 @@
<a href="#" class="text-danger">Delete</a>
</small>
</td>
<td>/{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}</td>
<td>{{ pg.updated_on | date: "MMMM d, yyyy" }}</td>
</tr>
{%- endfor %}

View File

@ -1,6 +1,9 @@
<div class="home">
<article class="content auto">
{{ page.text }}
{% if logged_on -%}
<p><small><a href="/page/{{ page.id }}/edit">Edit This Page</a></small></p>
{% endif %}
</article>
<aside class="app-sidebar">
<div>
@ -9,7 +12,7 @@
<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>
<a href="https://prayer.bitbadger.solutions" title="PrayerTracker" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">
A prayer request tracking website (Free for any church or Sunday School class!)
@ -19,14 +22,14 @@
<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>
<a href="https://prayerjournal.me" title="myPrayerJournal" target="_blank" rel="noopener">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>
<a href="https://blog.bitbadger.solutions/linux/" title="Linux Resources" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">Handy information for Linux folks</p>
</div>
@ -37,7 +40,7 @@
<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>
<a href="https://www.futilitycloset.com" title="Futility Closet" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">An idlers miscellany of compendious amusements</p>
</div>
@ -45,7 +48,7 @@
<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>
<a href="https://mindymackenzie.com" title="Mindy Mackenzie" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p>
</div>
@ -53,7 +56,7 @@
<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>
<a href="http://riehlworldview.com" title="Riehl World News" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">Riehl news for real people</p>
</div>
@ -64,7 +67,7 @@
<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>
<a href="https://bayvista.org" title="Bay Vista Baptist Church" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">Biloxi, Mississippi</p>
</div>
@ -72,7 +75,7 @@
<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>
<a href="https://blog.bitbadger.solutions" title="The Bit Badger Blog" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p>
</div>
@ -82,14 +85,14 @@
<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>
<a href="https://daniel.summershome.org" title="Daniel J. Summers" target="_blank" rel="noopener">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>
<a href="https://devotions.summershome.org" title="A Word from the Word" target="_blank" rel="noopener">Visit</a>
</p>
<p class="app-sidebar-description">Devotions by Daniel</p>
</div>

View File

@ -1,10 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width">
<meta name="generator" content="{{ generator }}">
<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">
<link rel="icon" href="/themes/{{ web_log.theme_path }}/favicon.ico">
</head>
<body>
<header class="site-header">
@ -37,7 +39,7 @@
{% endif %}
</small>
</div>
<div>
<div class="footer-by">
A <strong><a href="/">Bit Badger Solutions</a></strong> original design
</div>
</footer>

View File

@ -2,4 +2,7 @@
<h1>{{ page.title }}</h1>
{{ page.text }}
<p><br><a href="/" title="Home">&laquo; Home</a></p>
{% if logged_on -%}
<p><small><a href="/page/{{ page.id }}/edit">Edit This Page</a></small></p>
{% endif %}
</article>

View File

@ -0,0 +1,113 @@
<h1 class="solution-header">
{{ page.title }}<br>
<small><small>
{%- assign url = page.metadata | value: "url" -%}
{%- assign no_link = page.metadata | value: "no_link" -%}
{%- assign archive = page.metadata | where: "name", "archive_url" | size -%}
{% if no_link == "true" -%}
{{ url }}
{%- else -%}
<a href="{{ url }}" target="_blank" rel="noopener">{{ url }}</a>
{%- endif %}
{% if archive > 0 -%}
&nbsp;&nbsp;
<a href="{{ page.metadata | value: "archive_url" }}" target="_blank" rel="noopener"><small>(Archive)</small></a>
{%- endif %}
</small></small>
</h1>
<div class="app-info">
<article class="content">
<aside>
<span>&nbsp;</span>
<img src="/themes/{{ web_log.theme_path }}/screenshots/{{ page.permalink | split: "/" | last }}.png"
alt="Screen shot of {{ page.title | escape }}">
</aside>
{{ page.text }}
{%- assign curr_tech = page.metadata | where: "name", "tech" -%}
{%- assign past_tech = page.metadata | where: "name", "past_tech" -%}
{%- assign curr_count = curr_tech | size -%}
{%- assign past_count = past_tech | size -%}
{% if curr_count > 0 or past_count > 0 -%}
{% comment %} TODO / WIP
{% capture all_links -%}
ASP.NET MVC|https://dotnet.microsoft.com/apps/aspnet/mvc,
Azure|https://azure.microsoft.com/,
BlogEngine.NET|http://www.dotnetblogengine.net/,
Database Abstraction|https://github.com/danieljsummers/DatabaseAbstraction,
Digital Ocean|https://www.digitalocean.com/,
Giraffe|https://github.com/giraffe-fsharp/Giraffe,
GitHub|https://github.com/,
GitHub Pages|https://pages.github.com/,
Hexo|https://hexo.io/,
Hugo|https://gohugo.io/,
Jekyll|https://jekyllrb.com/,
MongoDB|https://www.mongodb.com/,
MySQL|https://www.mysql.com/,
myWebLog|https://github.com/bit-badger/myWebLog,
nginx|http://nginx.org/,
Orchard|https://orchardproject.net/,
PHP|https://www.php.net/,
PostgreSQL|https://www.postgresql.org/,
Rackspace Cloud|https://www.rackspace.com/cloud,
RavenDB|https://ravendb.net/,
RethinkDB|https://rethinkdb.com/,
SQL Server|https://www.microsoft.com/en-us/sql-server/,
Vue.js|https://vuejs.org/,
WordPress|https://wordpress.org
{%- endcapture %}
{% endcomment %}
<section>
<h3 onclick="toggle('techStack')">
The Technology Stack<span id="techStackArrow" class="arrow">&#x25BC;</span>
</h3>
<div id="techStack" style="display:none;">
{% if curr_count > 0 -%}
{% if past_count > 0 -%}
<p><small><strong>Current:</strong></small></p>
{%- endif %}
<ul>
{% for curr in curr_tech -%}
{%- assign tech = curr.value | split: "|" -%}
<li>
{% comment %} <a v-if="hasLink(tech[0])" :href="techLinks[tech[0]]" target="_blank">{{ tech[0] }}</a> {% endcomment %}
{{ tech[0] }} for {{ tech[1] }}
</li>
{%- endfor %}
</ul>
{%- endif %}
{% if past_count > 0 -%}
{% if curr_count > 0 %}
<p><small><strong>Previously:</strong></small></p>
{% endif %}
<ul>
{% for past in past_tech -%}
{%- assign tech = past.value | split: "|" -%}
<li>
{% comment %} <a v-if="hasLink(tech[0])" :href="techLinks[tech[0]]" target="_blank">{{ tech[0] }}</a> {% endcomment %}
{{ tech[0] }} for {{ tech[1] }}
</li>
{%- endfor %}
</ul>
{% endif %}
</div>
</section>
{%- endif %}
<p><br><a href="/solutions">&laquo; Back to All Solutions</a></p>
{% if logged_on -%}
<p><small><a href="/page/{{ page.id }}/edit">Edit This Page</a></small></p>
{% endif %}
</article>
</div>
<script>
function toggle(id) {
const section = document.getElementById(id)
const arrow = document.getElementById(`${id}Arrow`)
if (section.style.display === "none") {
section.style.display = "block"
arrow.innerHTML = "&#x25B2;"
} else {
section.style.display = "none"
arrow.innerHTML = "&#x25BC;"
}
}
</script>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="generator" content="myWebLog 2">
<meta name="generator" content="{{ generator }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css">

View File

@ -77,6 +77,7 @@
newRow.appendChild(valueCol)
document.getElementById("metaItems").appendChild(newRow)
document.getElementById(nameField.id).focus()
this.nextMetaIndex++
},

View File

@ -19,7 +19,7 @@ a img {
acronym {
border-bottom: dotted 1px black;
}
header, h1, h2, h3, footer a {
header, h1, h2, h3, .footer-by a {
font-family: "Oswald", "Segoe UI", Ubuntu, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
}
h1 {
@ -201,6 +201,48 @@ abbr[title] {
font-weight: bold;
color: maroon;
}
/* Individual solution pages */
h1.solution-header {
line-height: 1.6rem;
}
.app-info aside {
float: right;
background-color: #FFFAFA;
}
.app-info aside > span {
padding-left: .75rem;
}
.app-info aside > img {
overflow: hidden;
border: dotted 1px darkgray;
border-radius: 10px;
}
.tech-stack p {
margin-bottom: 0;
}
.tech-stack ul {
margin-top: 0;
}
blockquote {
border-left: solid 1px darkgray;
margin-left: 25px;
padding-left: 15px;
}
.quote {
font-style: italic;
}
.source {
text-align: right;
padding-right: 60px;
}
.app-info h3:hover {
cursor: hand;
cursor: pointer;
}
.app-info .arrow {
font-size: .75rem;
padding-left: 1rem;
}
/* Footer */
footer {
display: flex;