oliverbooth.dev/OliverBooth/Pages/Blog/Article.cshtml

215 lines
6.6 KiB
Plaintext

@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
@using Humanizer
@using Markdig
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OliverBooth.Common.Data
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Services
@inject IBlogPostService BlogPostService
@inject MarkdownPipeline MarkdownPipeline
@model Article
@if (Model.ShowPasswordPrompt)
{
<div class="callout" data-callout="danger">
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;
}
@if (Model.Post is not { } post)
{
return;
}
@{
ViewData["Post"] = post;
ViewData["Title"] = post.Title;
IBlogAuthor author = post.Author;
DateTimeOffset published = post.Published;
}
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a asp-page="Index">Blog</a>
</li>
<li class="breadcrumb-item active" aria-current="page">@post.Title</li>
</ol>
</nav>
@switch (post.Visibility)
{
case Visibility.Private:
<div class="callout" data-callout="danger">
This post is private and can only be viewed by those with the password.
</div>
break;
case Visibility.Unlisted:
<div class="callout" data-callout="warning">
This post is unlisted and can only be viewed by those with the link.
</div>
break;
}
<h1>@post.Title</h1>
<p class="text-muted">
<img class="blog-author-icon" src="@author.AvatarUrl" alt="@author.DisplayName">
@author.DisplayName &bull;
<abbr data-bs-toggle="tooltip" data-bs-title="@published.ToString("dddd, d MMMM yyyy HH:mm")">
Published @published.Humanize()
</abbr>
@if (post.Updated is { } updated)
{
<span>&bull;</span>
<abbr data-bs-toggle="tooltip" data-bs-title="@updated.ToString("dddd, d MMMM yyyy HH:mm")">
Updated @updated.Humanize()
</abbr>
}
</p>
<hr>
<article>
@Html.Raw(BlogPostService.RenderPost(post))
</article>
<hr>
<div class="d-flex align-items-center mb-3">
<i data-lucide="tag"></i>
<ul class="ms-2 post-tags">
@foreach (string tag in post.Tags)
{
<li class="post-tag">
<a asp-page="Index" asp-route-tag="@Html.UrlEncoder.Encode(tag)">@tag</a>
</li>
}
</ul>
</div>
<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>
@if (post.EnableComments)
{
<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>
}
}
else
{
<p class="text-center text-muted">Comments are not enabled for this post.</p>
}
@{
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 &bull;
<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>
}
}
}