WIP on Bit Badger theme

Building it in-place for now; will move it to its own assembly before going live
This commit is contained in:
Daniel J. Summers 2022-03-06 23:21:05 -05:00
parent d0cc9d0d7c
commit 7e5e693008
45 changed files with 1362 additions and 7 deletions

View File

@ -55,7 +55,7 @@ public static class PageExtensions
/// <param name="pageNbr">The page number to retrieve</param> /// <param name="pageNbr">The page number to retrieve</param>
/// <returns>The pages</returns> /// <returns>The pages</returns>
public static async Task<List<Page>> FindPageOfPages(this DbSet<Page> db, int pageNbr) => public static async Task<List<Page>> FindPageOfPages(this DbSet<Page> db, int pageNbr) =>
await db.Skip((pageNbr - 1) * 50).Take(25).ToListAsync().ConfigureAwait(false); await db.OrderBy(p => p.Title).Skip((pageNbr - 1) * 25).Take(25).ToListAsync().ConfigureAwait(false);
/// <summary> /// <summary>
/// Retrieve a page by its ID (tracked) /// Retrieve a page by its ID (tracked)

View File

@ -11,7 +11,7 @@ using MyWebLog.Data;
namespace MyWebLog.Data.Migrations namespace MyWebLog.Data.Migrations
{ {
[DbContext(typeof(WebLogDbContext))] [DbContext(typeof(WebLogDbContext))]
[Migration("20220227160816_Initial")] [Migration("20220307034307_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -123,6 +123,9 @@ namespace MyWebLog.Data.Migrations
b.Property<bool>("ShowInPageList") b.Property<bool>("ShowInPageList")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Template")
.HasColumnType("TEXT");
b.Property<string>("Text") b.Property<string>("Text")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@ -87,6 +87,7 @@ namespace MyWebLog.Data.Migrations
PublishedOn = table.Column<DateTime>(type: "TEXT", nullable: false), PublishedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false), UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
ShowInPageList = table.Column<bool>(type: "INTEGER", nullable: false), ShowInPageList = table.Column<bool>(type: "INTEGER", nullable: false),
Template = table.Column<string>(type: "TEXT", nullable: true),
Text = table.Column<string>(type: "TEXT", nullable: false) Text = table.Column<string>(type: "TEXT", nullable: false)
}, },
constraints: table => constraints: table =>

View File

@ -121,6 +121,9 @@ namespace MyWebLog.Data.Migrations
b.Property<bool>("ShowInPageList") b.Property<bool>("ShowInPageList")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Template")
.HasColumnType("TEXT");
b.Property<string>("Text") b.Property<string>("Text")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@ -45,6 +45,11 @@ public class Page
/// </summary> /// </summary>
public bool ShowInPageList { get; set; } = false; public bool ShowInPageList { get; set; } = false;
/// <summary>
/// The template to use when rendering this page
/// </summary>
public string? Template { get; set; } = null;
/// <summary> /// <summary>
/// The current text of the page /// The current text of the page
/// </summary> /// </summary>

View File

@ -10,6 +10,11 @@ public class SinglePageModel : MyWebLogModel
/// </summary> /// </summary>
public Page Page { get; init; } public Page Page { get; init; }
/// <summary>
/// Is this the home page?
/// </summary>
public bool IsHome => Page.Id == WebLog.DefaultPage;
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>

View File

@ -21,7 +21,7 @@ public class PostController : MyWebLogController
if (WebLog.DefaultPage == "posts") return await PageOfPosts(1); if (WebLog.DefaultPage == "posts") return await PageOfPosts(1);
var page = await Db.Pages.FindById(WebLog.DefaultPage); var page = await Db.Pages.FindById(WebLog.DefaultPage);
return page is null ? NotFound() : ThemedView("SinglePage", new SinglePageModel(page, WebLog)); return page is null ? NotFound() : ThemedView(page.Template ?? "SinglePage", new SinglePageModel(page, WebLog));
} }
[HttpGet("~/page/{pageNbr:int}")] [HttpGet("~/page/{pageNbr:int}")]
@ -42,7 +42,7 @@ public class PostController : MyWebLogController
var page = await Db.Pages.FindByPermalink(permalink); var page = await Db.Pages.FindByPermalink(permalink);
if (page != null) if (page != null)
{ {
return ThemedView("SinglePage", new SinglePageModel(page, WebLog)); return ThemedView(page.Template ?? "SinglePage", new SinglePageModel(page, WebLog));
} }
// TOOD: search prior permalinks for posts and pages // TOOD: search prior permalinks for posts and pages

View File

@ -34,6 +34,8 @@ public abstract class MyWebLogController : Controller
protected ViewResult ThemedView(string template, object model) protected ViewResult ThemedView(string template, object model)
{ {
// TODO: get actual version
ViewBag.Version = "2";
return View(template, model); return View(template, model);
} }
} }

View File

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Text.Encodings.Web;
namespace MyWebLog.Features.Shared.TagHelpers;
/// <summary>
/// Image tag helper to load a theme's image
/// </summary>
[HtmlTargetElement("img", Attributes = "asp-theme")]
public class ImageTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.ImageTagHelper
{
/// <summary>
/// The theme for which the image should be loaded
/// </summary>
[HtmlAttributeName("asp-theme")]
public string Theme { get; set; } = "";
/// <inheritdoc />
public ImageTagHelper(IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder,
IUrlHelperFactory urlHelperFactory)
: base(fileVersionProvider, htmlEncoder, urlHelperFactory) { }
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (Theme == "")
{
base.Process(context, output);
return;
}
output.Attributes.SetAttribute("src", $"~/img/{Theme}/{context.AllAttributes["src"]?.Value}");
ProcessUrlAttribute("src", output);
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Mvc.Razor.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Text.Encodings.Web;
namespace MyWebLog.Features.Shared.TagHelpers;
/// <summary>
/// Tag helper to link stylesheets for a theme
/// </summary>
[HtmlTargetElement("link", Attributes = "asp-theme")]
public class LinkTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.LinkTagHelper
{
/// <summary>
/// The theme for which a style sheet should be loaded
/// </summary>
[HtmlAttributeName("asp-theme")]
public string Theme { get; set; } = "";
/// <summary>
/// The style sheet to be loaded (defaults to "style")
/// </summary>
[HtmlAttributeName("asp-style")]
public string Style { get; set; } = "style";
/// <inheritdoc />
public LinkTagHelper(IWebHostEnvironment hostingEnvironment, TagHelperMemoryCacheProvider cacheProvider,
IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder, JavaScriptEncoder javaScriptEncoder,
IUrlHelperFactory urlHelperFactory)
: base(hostingEnvironment, cacheProvider, fileVersionProvider, htmlEncoder, javaScriptEncoder, urlHelperFactory)
{ }
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (Theme == "")
{
base.Process(context, output);
return;
}
switch (context.AllAttributes["rel"]?.Value.ToString())
{
case "stylesheet":
output.Attributes.SetAttribute("href", $"~/css/{Theme}/{Style}.css");
break;
case "icon":
output.Attributes.SetAttribute("type", "image/x-icon");
output.Attributes.SetAttribute("href", $"~/img/{Theme}/favicon.ico");
break;
}
ProcessUrlAttribute("href", output);
}
}

View File

@ -6,6 +6,14 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Content Remove="Themes\BitBadger\solutions.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Themes\BitBadger\solutions.json" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="wwwroot\img\" /> <Folder Include="wwwroot\img\" />
</ItemGroup> </ItemGroup>

View File

@ -43,7 +43,7 @@ builder.Services.AddDbContext<WebLogDbContext>(o =>
{ {
// TODO: can get from DI? // TODO: can get from DI?
var db = WebLogCache.HostToDb(new HttpContextAccessor().HttpContext!); var db = WebLogCache.HostToDb(new HttpContextAccessor().HttpContext!);
// "Data Source=Db/empty.db" // "empty";
o.UseSqlite($"Data Source=Db/{db}.db"); o.UseSqlite($"Data Source=Db/{db}.db");
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<footer> <footer>
<hr> <hr>
<div class="container-fluid text-end"> <div class="container-fluid text-end">
<img src="img/logo-dark.png" alt="myWebLog"> <img src="~/img/logo-dark.png" alt="myWebLog">
</div> </div>
</footer> </footer>

View File

@ -2,11 +2,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<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="myWebLog 2">
<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="~/css/@Model.WebLog.ThemePath/style.css"> <link asp-theme="@Model.WebLog.ThemePath" />
@await RenderSectionAsync("Style", false) @await RenderSectionAsync("Style", false)
<title>@ViewBag.Title &laquo; @Model.WebLog.Name</title> <title>@ViewBag.Title &laquo; @Model.WebLog.Name</title>
</head> </head>

View File

@ -1 +1,3 @@
@namespace MyWebLog.Themes @namespace MyWebLog.Themes
@addTagHelper *, MyWebLog

View File

@ -0,0 +1,262 @@
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.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB