Migrate bit-badger theme
- Add generator string - Focus meta name when adding new item - Sort meta items
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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> • </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>
|
|
@ -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 « @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">
|
||||
|
||||
</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> <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> •
|
||||
@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>
|
|
@ -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>
|
|
@ -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");
|
||||
}
|
|
@ -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>
|
|
@ -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 “rising star” 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’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’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’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 (“geek stuff”) 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’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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,6 +0,0 @@
|
|||
@namespace MyWebLog.Themes
|
||||
|
||||
@using MyWebLog.Features.Shared
|
||||
@using MyWebLog.Properties
|
||||
|
||||
@addTagHelper *, MyWebLog
|
|
@ -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;
|
||||
}
|
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
open MyWebLog
|
||||
open Suave
|
||||
|
||||
startWebServer defaultConfig (Successful.OK (Strings.get "LastUpdated"))
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
@ -94,6 +95,16 @@ module private Helpers =
|
|||
| 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
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
"RethinkDB.DistributedCache": "Debug",
|
||||
"RethinkDb.Driver": "Debug"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Generator": "myWebLog 2.0-alpha01"
|
||||
}
|
||||
|
|
|
@ -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 }} « Admin « {{ 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">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 • 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 • 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 • 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 idler’s 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 • 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 • 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 • 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 • 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">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>
|
||||
<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>
|
||||
|
|
|
@ -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 }} » 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>
|
||||
|
|
|
@ -2,4 +2,7 @@
|
|||
<h1>{{ page.title }}</h1>
|
||||
{{ page.text }}
|
||||
<p><br><a href="/" title="Home">« Home</a></p>
|
||||
{% if logged_on -%}
|
||||
<p><small><a href="/page/{{ page.id }}/edit">Edit This Page</a></small></p>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
|
113
src/MyWebLog/themes/bit-badger/solution-page.liquid
Normal 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 -%}
|
||||
|
||||
<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> </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">▼</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">« 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 = "▲"
|
||||
} else {
|
||||
section.style.display = "none"
|
||||
arrow.innerHTML = "▼"
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -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">
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
newRow.appendChild(valueCol)
|
||||
|
||||
document.getElementById("metaItems").appendChild(newRow)
|
||||
document.getElementById(nameField.id).focus()
|
||||
this.nextMetaIndex++
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|