2023-08-13 18:12:13 +01:00
|
|
|
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
|
2023-08-08 00:28:33 +01:00
|
|
|
@using Humanizer
|
2024-05-01 16:47:31 +01:00
|
|
|
@using Markdig
|
2024-02-23 16:50:43 +00:00
|
|
|
@using OliverBooth.Data
|
2023-08-13 17:33:54 +01:00
|
|
|
@using OliverBooth.Data.Blog
|
2023-09-19 19:29:21 +01:00
|
|
|
@using OliverBooth.Services
|
|
|
|
@inject IBlogPostService BlogPostService
|
2024-05-01 16:47:31 +01:00
|
|
|
@inject MarkdownPipeline MarkdownPipeline
|
2023-08-12 20:13:47 +01:00
|
|
|
@model Article
|
2023-08-06 15:57:23 +01:00
|
|
|
|
2023-09-26 12:46:18 +01:00
|
|
|
@if (Model.ShowPasswordPrompt)
|
|
|
|
{
|
|
|
|
<div class="alert alert-danger" role="alert">
|
|
|
|
This post is private and can only be viewed by those with the password.
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<form method="post">
|
|
|
|
<div class="mb-3">
|
|
|
|
<label for="password" class="form-label">Password</label>
|
|
|
|
<input type="password" class="form-control" id="password" name="password" required>
|
|
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">Submit</button>
|
|
|
|
</form>
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-08-08 01:34:27 +01:00
|
|
|
@if (Model.Post is not { } post)
|
2023-08-06 15:57:23 +01:00
|
|
|
{
|
2023-08-08 01:34:27 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-08-08 21:01:13 +01:00
|
|
|
@{
|
2023-08-14 00:58:58 +01:00
|
|
|
ViewData["Post"] = post;
|
2023-08-11 02:08:03 +01:00
|
|
|
ViewData["Title"] = post.Title;
|
2023-08-12 20:13:47 +01:00
|
|
|
IBlogAuthor author = post.Author;
|
2023-08-08 21:01:13 +01:00
|
|
|
DateTimeOffset published = post.Published;
|
|
|
|
}
|
|
|
|
|
2023-08-08 01:34:27 +01:00
|
|
|
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
|
|
|
|
<ol class="breadcrumb">
|
|
|
|
<li class="breadcrumb-item">
|
2023-08-16 14:08:33 +01:00
|
|
|
<a asp-page="Index">Blog</a>
|
2023-08-08 01:34:27 +01:00
|
|
|
</li>
|
|
|
|
<li class="breadcrumb-item active" aria-current="page">@post.Title</li>
|
|
|
|
</ol>
|
|
|
|
</nav>
|
2023-08-08 00:28:33 +01:00
|
|
|
|
2023-09-20 14:49:02 +01:00
|
|
|
@switch (post.Visibility)
|
|
|
|
{
|
2024-02-23 16:50:43 +00:00
|
|
|
case Visibility.Private:
|
2023-09-20 14:49:02 +01:00
|
|
|
<div class="alert alert-danger" role="alert">
|
|
|
|
This post is private and can only be viewed by those with the password.
|
|
|
|
</div>
|
|
|
|
break;
|
|
|
|
|
2024-02-23 16:50:43 +00:00
|
|
|
case Visibility.Unlisted:
|
2023-09-20 14:49:02 +01:00
|
|
|
<div class="alert alert-warning" role="alert">
|
|
|
|
This post is unlisted and can only be viewed by those with the link.
|
|
|
|
</div>
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-08-08 01:34:27 +01:00
|
|
|
<h1>@post.Title</h1>
|
|
|
|
<p class="text-muted">
|
2023-08-12 20:13:47 +01:00
|
|
|
<img class="blog-author-icon" src="@author.AvatarUrl" alt="@author.DisplayName">
|
2023-08-12 14:24:27 +01:00
|
|
|
@author.DisplayName •
|
2023-08-08 21:01:13 +01:00
|
|
|
|
2023-08-10 15:32:34 +01:00
|
|
|
<abbr data-bs-toggle="tooltip" data-bs-title="@published.ToString("dddd, d MMMM yyyy HH:mm")">
|
2023-08-08 21:01:13 +01:00
|
|
|
Published @published.Humanize()
|
2023-08-08 01:34:27 +01:00
|
|
|
</abbr>
|
2023-08-08 21:01:13 +01:00
|
|
|
|
2023-08-08 02:06:38 +01:00
|
|
|
@if (post.Updated is { } updated)
|
|
|
|
{
|
|
|
|
<span>•</span>
|
2023-08-10 15:32:34 +01:00
|
|
|
<abbr data-bs-toggle="tooltip" data-bs-title="@updated.ToString("dddd, d MMMM yyyy HH:mm")">
|
2023-08-08 02:06:38 +01:00
|
|
|
Updated @updated.Humanize()
|
|
|
|
</abbr>
|
|
|
|
}
|
2023-08-08 01:34:27 +01:00
|
|
|
</p>
|
2024-02-19 23:01:32 +00:00
|
|
|
<div class="post-tags">
|
2023-09-23 22:08:25 +01:00
|
|
|
@foreach (string tag in post.Tags)
|
|
|
|
{
|
|
|
|
<a asp-page="Index" asp-route-tag="@tag" class="badge bg-secondary">@tag</a>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
<hr>
|
2023-08-08 01:34:27 +01:00
|
|
|
|
2023-09-28 10:58:59 +01:00
|
|
|
<article>
|
|
|
|
@Html.Raw(BlogPostService.RenderPost(post))
|
2023-08-08 01:34:27 +01:00
|
|
|
</article>
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
2023-09-19 19:29:21 +01:00
|
|
|
<div class="row">
|
|
|
|
<div class="col-sm-12 col-md-6">
|
|
|
|
@if (BlogPostService.GetPreviousPost(post) is { } previousPost)
|
|
|
|
{
|
|
|
|
<small>Previous Post</small>
|
|
|
|
<p class="lead">
|
|
|
|
<a asp-page="Article"
|
|
|
|
asp-route-year="@previousPost.Published.Year.ToString("0000")"
|
|
|
|
asp-route-month="@previousPost.Published.Month.ToString("00")"
|
|
|
|
asp-route-day="@previousPost.Published.Day.ToString("00")"
|
|
|
|
asp-route-slug="@previousPost.Slug">
|
|
|
|
@previousPost.Title
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
<div class="col-sm-12 col-md-6" style="text-align: right;">
|
|
|
|
@if (BlogPostService.GetNextPost(post) is { } nextPost)
|
|
|
|
{
|
|
|
|
<small>Next Post</small>
|
|
|
|
<p class="lead">
|
|
|
|
<a asp-page="Article"
|
|
|
|
asp-route-year="@nextPost.Published.Year.ToString("0000")"
|
|
|
|
asp-route-month="@nextPost.Published.Month.ToString("00")"
|
|
|
|
asp-route-day="@nextPost.Published.Day.ToString("00")"
|
|
|
|
asp-route-slug="@nextPost.Slug">
|
|
|
|
@nextPost.Title
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<hr>
|
|
|
|
|
2023-08-08 01:34:27 +01:00
|
|
|
@if (post.EnableComments)
|
|
|
|
{
|
2024-04-27 16:19:06 +01:00
|
|
|
<div class="giscus"></div>
|
|
|
|
@section Scripts
|
|
|
|
{
|
|
|
|
<script src="https://giscus.app/client.js"
|
|
|
|
data-repo="oliverbooth/oliverbooth.dev"
|
|
|
|
data-repo-id="MDEwOlJlcG9zaXRvcnkyNDUxODEyNDI="
|
|
|
|
data-category="Comments"
|
|
|
|
data-category-id="DIC_kwDODp0rOs4Ce_Nj"
|
|
|
|
data-mapping="pathname"
|
|
|
|
data-strict="0"
|
|
|
|
data-reactions-enabled="1"
|
|
|
|
data-emit-metadata="0"
|
|
|
|
data-input-position="bottom"
|
|
|
|
data-theme="preferred_color_scheme"
|
|
|
|
data-lang="en"
|
|
|
|
crossorigin="anonymous"
|
|
|
|
async>
|
|
|
|
</script>
|
|
|
|
}
|
2024-05-01 16:47:31 +01:00
|
|
|
|
|
|
|
int commentCount = BlogPostService.GetLegacyCommentCount(post);
|
|
|
|
if (commentCount > 0)
|
|
|
|
{
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
var nestLevelMap = new Dictionary<ILegacyComment, int>();
|
|
|
|
IReadOnlyList<ILegacyComment> legacyComments = BlogPostService.GetLegacyComments(post);
|
|
|
|
var commentStack = new Stack<ILegacyComment>(legacyComments.OrderByDescending(c => c.CreatedAt));
|
|
|
|
<p class="text-center">
|
|
|
|
<strong>@("legacy comment".ToQuantity(commentCount))</strong>
|
|
|
|
</p>
|
|
|
|
<p class="text-center">
|
|
|
|
<sub>Legacy comments are comments that were posted using a commenting system that I no longer use. This exists for posterity.</sub>
|
|
|
|
</p>
|
|
|
|
|
|
|
|
while (commentStack.Count > 0)
|
|
|
|
{
|
|
|
|
ILegacyComment comment = commentStack.Pop();
|
|
|
|
foreach (ILegacyComment reply in BlogPostService.GetLegacyReplies(comment).OrderByDescending(c => c.CreatedAt))
|
|
|
|
{
|
|
|
|
if (nestLevelMap.TryGetValue(comment, out int currentLevel))
|
|
|
|
{
|
|
|
|
nestLevelMap[reply] = currentLevel + 1;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
nestLevelMap[reply] = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
commentStack.Push(reply);
|
|
|
|
}
|
|
|
|
|
|
|
|
int padding = 0;
|
|
|
|
if (nestLevelMap.TryGetValue(comment, out int nestLevel))
|
|
|
|
{
|
|
|
|
padding = 50 * nestLevel;
|
|
|
|
}
|
|
|
|
|
|
|
|
<div class="legacy-comment" style="margin-left: @(padding)px;">
|
|
|
|
<img class="blog-author-icon" src="@comment.GetAvatarUrl()" alt="@comment.Author">
|
|
|
|
@comment.Author •
|
|
|
|
|
|
|
|
<abbr class="text-muted" data-bs-toggle="tooltip" data-bs-title="@comment.CreatedAt.ToString("dddd, d MMMM yyyy HH:mm")">
|
|
|
|
@comment.CreatedAt.Humanize()
|
|
|
|
</abbr>
|
|
|
|
|
|
|
|
<div class="comment">@Html.Raw(Markdown.ToHtml(comment.Body, MarkdownPipeline))</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
2023-08-08 01:34:27 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
<p class="text-center text-muted">Comments are not enabled for this post.</p>
|
2023-08-08 02:06:38 +01:00
|
|
|
}
|