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
|
withTable Table.Page
|
||||||
getAll [ webLogId ] (nameof webLogId)
|
getAll [ webLogId ] (nameof webLogId)
|
||||||
without [ "priorPermalinks"; "revisions" ]
|
without [ "priorPermalinks"; "revisions" ]
|
||||||
orderBy "title"
|
orderByFunc (fun row -> row.G("title").Downcase ())
|
||||||
skip ((pageNbr - 1) * 25)
|
skip ((pageNbr - 1) * 25)
|
||||||
limit 25
|
limit 25
|
||||||
result; withRetryDefault
|
result; withRetryDefault
|
||||||
|
|
|
@ -46,6 +46,25 @@ type DisplayPage =
|
||||||
|
|
||||||
/// Is this the default page?
|
/// Is this the default page?
|
||||||
isDefault : bool
|
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
|
/// Create a display page from a database page
|
||||||
static member fromPage webLog (page : Page) =
|
static member fromPage webLog (page : Page) =
|
||||||
|
@ -57,6 +76,8 @@ type DisplayPage =
|
||||||
updatedOn = page.updatedOn
|
updatedOn = page.updatedOn
|
||||||
showInPageList = page.showInPageList
|
showInPageList = page.showInPageList
|
||||||
isDefault = pageId = webLog.defaultPage
|
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
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.1.32210.238
|
VisualStudioVersion = 17.1.32210.238
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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}"
|
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}"
|
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
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
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.ActiveCfg = Debug|Any CPU
|
||||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = 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
|
{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 =
|
module private Helpers =
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Antiforgery
|
open Microsoft.AspNetCore.Antiforgery
|
||||||
|
open Microsoft.Extensions.Configuration
|
||||||
open Microsoft.Extensions.DependencyInjection
|
open Microsoft.Extensions.DependencyInjection
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
open System.IO
|
open System.IO
|
||||||
|
@ -94,6 +95,16 @@ module private Helpers =
|
||||||
| None -> return [||]
|
| 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
|
/// 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 =
|
let private deriveWebLogFromHash (hash : Hash) ctx =
|
||||||
match hash.ContainsKey "web_log" with
|
match hash.ContainsKey "web_log" with
|
||||||
|
@ -112,6 +123,7 @@ module private Helpers =
|
||||||
hash.Add ("page_list", PageListCache.get ctx)
|
hash.Add ("page_list", PageListCache.get ctx)
|
||||||
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
||||||
hash.Add ("messages", messages)
|
hash.Add ("messages", messages)
|
||||||
|
hash.Add ("generator", generator ctx)
|
||||||
|
|
||||||
do! commitSession ctx
|
do! commitSession ctx
|
||||||
|
|
||||||
|
@ -343,7 +355,7 @@ module Page =
|
||||||
let! pages = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx)
|
let! pages = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx)
|
||||||
return!
|
return!
|
||||||
Hash.FromAnonymousObject
|
Hash.FromAnonymousObject
|
||||||
{| pages = pages |> List.map (DisplayPage.fromPage webLog)
|
{| pages = pages |> List.map (DisplayPage.fromPageMinimal webLog)
|
||||||
page_title = "Pages"
|
page_title = "Pages"
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "page-list" next ctx
|
|> viewForTheme "admin" "page-list" next ctx
|
||||||
|
@ -414,6 +426,7 @@ module Page =
|
||||||
metadata = Seq.zip model.metaNames model.metaValues
|
metadata = Seq.zip model.metaNames model.metaValues
|
||||||
|> Seq.filter (fun it -> fst it > "")
|
|> Seq.filter (fun it -> fst it > "")
|
||||||
|> Seq.map (fun it -> { name = fst it; value = snd it })
|
|> Seq.map (fun it -> { name = fst it; value = snd it })
|
||||||
|
|> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}")
|
||||||
|> List.ofSeq
|
|> List.ofSeq
|
||||||
revisions = revision :: page.revisions
|
revisions = revision :: page.revisions
|
||||||
}
|
}
|
||||||
|
@ -482,7 +495,7 @@ module Post =
|
||||||
match! Data.Page.findById (PageId pageId) webLog.id (conn ctx) with
|
match! Data.Page.findById (PageId pageId) webLog.id (conn ctx) with
|
||||||
| Some page ->
|
| Some page ->
|
||||||
return!
|
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
|
|> themedView (defaultArg page.template "single-page") next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
@ -501,7 +514,7 @@ module Post =
|
||||||
match! Data.Page.findByPermalink permalink webLog.id conn with
|
match! Data.Page.findByPermalink permalink webLog.id conn with
|
||||||
| Some page ->
|
| Some page ->
|
||||||
return!
|
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
|
|> themedView (defaultArg page.template "single-page") next ctx
|
||||||
| None ->
|
| None ->
|
||||||
// Prior post
|
// Prior post
|
||||||
|
|
|
@ -8,5 +8,6 @@
|
||||||
"RethinkDB.DistributedCache": "Debug",
|
"RethinkDB.DistributedCache": "Debug",
|
||||||
"RethinkDb.Driver": "Debug"
|
"RethinkDb.Driver": "Debug"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"Generator": "myWebLog 2.0-alpha01"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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>
|
<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"
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<div class="form-floating pb-3">
|
<div class="form-floating pb-3">
|
||||||
<select name="template" id="template" class="form-control">
|
<select name="template" id="template" class="form-control">
|
||||||
{% for tmpl in templates -%}
|
{% 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] }}
|
{{ tmpl[1] }}
|
||||||
</option>
|
</option>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Title</th>
|
<th scope="col">Title</th>
|
||||||
|
<th scope="col">Permalink</th>
|
||||||
<th scope="col">Last Updated</th>
|
<th scope="col">Last Updated</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
<a href="#" class="text-danger">Delete</a>
|
<a href="#" class="text-danger">Delete</a>
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
|
<td>/{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}</td>
|
||||||
<td>{{ pg.updated_on | date: "MMMM d, yyyy" }}</td>
|
<td>{{ pg.updated_on | date: "MMMM d, yyyy" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<div class="home">
|
<div class="home">
|
||||||
<article class="content auto">
|
<article class="content auto">
|
||||||
{{ page.text }}
|
{{ page.text }}
|
||||||
|
{% if logged_on -%}
|
||||||
|
<p><small><a href="/page/{{ page.id }}/edit">Edit This Page</a></small></p>
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<div>
|
<div>
|
||||||
|
@ -9,7 +12,7 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>PrayerTracker</strong><br>
|
<strong>PrayerTracker</strong><br>
|
||||||
<a href="/solutions/prayer-tracker" title="About PrayerTracker • Bit Badger Solutions">About</a> •
|
<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>
|
||||||
<p class="app-sidebar-description">
|
<p class="app-sidebar-description">
|
||||||
A prayer request tracking website (Free for any church or Sunday School class!)
|
A prayer request tracking website (Free for any church or Sunday School class!)
|
||||||
|
@ -19,14 +22,14 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>myPrayerJournal</strong><br>
|
<strong>myPrayerJournal</strong><br>
|
||||||
<a href="/solutions/my-prayer-journal" title="About myPrayerJournal • Bit Badger Solutions">About</a> •
|
<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>
|
||||||
<p class="app-sidebar-description">Minimalist personal prayer journal</p>
|
<p class="app-sidebar-description">Minimalist personal prayer journal</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>Linux Resources</strong><br>
|
<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>
|
||||||
<p class="app-sidebar-description">Handy information for Linux folks</p>
|
<p class="app-sidebar-description">Handy information for Linux folks</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +40,7 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>Futility Closet</strong><br>
|
<strong>Futility Closet</strong><br>
|
||||||
<a href="/solutions/futility-closet" title="About Futility Closet • Bit Badger Solutions">About</a> •
|
<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>
|
||||||
<p class="app-sidebar-description">An idler’s miscellany of compendious amusements</p>
|
<p class="app-sidebar-description">An idler’s miscellany of compendious amusements</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +48,7 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>Mindy Mackenzie</strong><br>
|
<strong>Mindy Mackenzie</strong><br>
|
||||||
<a href="/solutions/mindy-mackenzie" title="About Mindy Mackenzie • Bit Badger Solutions">About</a> •
|
<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>
|
||||||
<p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p>
|
<p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,7 +56,7 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>Riehl World News</strong><br>
|
<strong>Riehl World News</strong><br>
|
||||||
<a href="/solutions/riehl-world-news" title="About Riehl World News • Bit Badger Solutions">About</a> •
|
<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>
|
||||||
<p class="app-sidebar-description">Riehl news for real people</p>
|
<p class="app-sidebar-description">Riehl news for real people</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,7 +67,7 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>Bay Vista Baptist Church</strong><br>
|
<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="/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>
|
||||||
<p class="app-sidebar-description">Biloxi, Mississippi</p>
|
<p class="app-sidebar-description">Biloxi, Mississippi</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +75,7 @@
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>The Bit Badger Blog</strong><br>
|
<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="/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>
|
||||||
<p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p>
|
<p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,14 +85,14 @@
|
||||||
<div>
|
<div>
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>Daniel J. Summers</strong><br>
|
<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>
|
||||||
<p class="app-sidebar-description">Daniel’s personal blog</p>
|
<p class="app-sidebar-description">Daniel’s personal blog</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="app-sidebar-name">
|
<p class="app-sidebar-name">
|
||||||
<strong>A Word from the Word</strong><br>
|
<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>
|
||||||
<p class="app-sidebar-description">Devotions by Daniel</p>
|
<p class="app-sidebar-description">Devotions by Daniel</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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>
|
<title>{{ page_title }} » Bit Badger Solutions</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald|Raleway">
|
<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="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css">
|
||||||
|
<link rel="icon" href="/themes/{{ web_log.theme_path }}/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
|
@ -37,7 +39,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="footer-by">
|
||||||
A <strong><a href="/">Bit Badger Solutions</a></strong> original design
|
A <strong><a href="/">Bit Badger Solutions</a></strong> original design
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -2,4 +2,7 @@
|
||||||
<h1>{{ page.title }}</h1>
|
<h1>{{ page.title }}</h1>
|
||||||
{{ page.text }}
|
{{ page.text }}
|
||||||
<p><br><a href="/" title="Home">« Home</a></p>
|
<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>
|
</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>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<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"
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css">
|
<link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css">
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
newRow.appendChild(valueCol)
|
newRow.appendChild(valueCol)
|
||||||
|
|
||||||
document.getElementById("metaItems").appendChild(newRow)
|
document.getElementById("metaItems").appendChild(newRow)
|
||||||
|
document.getElementById(nameField.id).focus()
|
||||||
this.nextMetaIndex++
|
this.nextMetaIndex++
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ a img {
|
||||||
acronym {
|
acronym {
|
||||||
border-bottom: dotted 1px black;
|
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;
|
font-family: "Oswald", "Segoe UI", Ubuntu, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -201,6 +201,48 @@ abbr[title] {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: maroon;
|
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 */
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|