V2 #1
|
@ -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,7 +46,26 @@ 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) =
|
||||||
let pageId = PageId.toString page.id
|
let pageId = PageId.toString page.id
|
||||||
|
@ -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;
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||||
|
@ -93,7 +94,17 @@ module private Helpers =
|
||||||
return msg |> (List.rev >> Array.ofList)
|
return msg |> (List.rev >> Array.ofList)
|
||||||
| 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
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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user