2024-02-20 20:36:23 +00:00
|
|
|
@page "/tutorial/{**slug}"
|
|
|
|
@using Humanizer
|
2024-05-01 16:47:31 +01:00
|
|
|
@using Markdig
|
2024-02-20 20:36:23 +00:00
|
|
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
2024-04-26 17:23:53 +01:00
|
|
|
@using OliverBooth.Data
|
2024-05-01 16:47:31 +01:00
|
|
|
@using OliverBooth.Data.Blog
|
2024-02-20 20:36:23 +00:00
|
|
|
@using OliverBooth.Data.Web
|
|
|
|
@using OliverBooth.Services
|
|
|
|
@inject ITutorialService TutorialService
|
2024-05-01 16:47:31 +01:00
|
|
|
@inject MarkdownPipeline MarkdownPipeline
|
2024-02-20 20:36:23 +00:00
|
|
|
@model Article
|
|
|
|
|
|
|
|
@if (Model.CurrentArticle is not { } article)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
@{
|
|
|
|
ViewData["Post"] = article;
|
|
|
|
ViewData["Title"] = article.Title;
|
|
|
|
DateTimeOffset published = article.Published;
|
|
|
|
}
|
|
|
|
|
|
|
|
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
|
|
|
|
<ol class="breadcrumb">
|
|
|
|
<li class="breadcrumb-item">
|
|
|
|
<a asp-page="/Tutorials/Index" asp-route-slug="">Tutorials</a>
|
|
|
|
</li>
|
|
|
|
@{
|
|
|
|
int? parentId = Model.CurrentArticle.Folder;
|
|
|
|
while (parentId is not null)
|
|
|
|
{
|
|
|
|
ITutorialFolder thisFolder = TutorialService.GetFolder(parentId.Value)!;
|
|
|
|
<li class="breadcrumb-item">
|
|
|
|
<a asp-page="/Tutorials/Index" asp-route-slug="@TutorialService.GetFullSlug(thisFolder)">
|
|
|
|
@thisFolder.Title
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
parentId = thisFolder.Parent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
<li class="breadcrumb-item actove" aria-current="page">@Model.CurrentArticle.Title</li>
|
|
|
|
</ol>
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
<h1>@article.Title</h1>
|
|
|
|
<p class="text-muted">
|
|
|
|
<abbr data-bs-toggle="tooltip" data-bs-title="@published.ToString("dddd, d MMMM yyyy HH:mm")">
|
|
|
|
Published @published.Humanize()
|
|
|
|
</abbr>
|
|
|
|
|
|
|
|
@if (article.Updated is { } updated)
|
|
|
|
{
|
|
|
|
<span>•</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(TutorialService.RenderArticle(article))
|
|
|
|
</article>
|
|
|
|
|
2024-04-27 17:00:20 +01:00
|
|
|
@if (article.HasOtherParts)
|
|
|
|
{
|
|
|
|
<hr>
|
2024-02-20 20:36:23 +00:00
|
|
|
|
2024-04-27 17:00:20 +01:00
|
|
|
<div class="row">
|
|
|
|
<div class="col-sm-12 col-md-6">
|
|
|
|
@if (article.PreviousPart is { } previousPartId &&
|
|
|
|
TutorialService.TryGetArticle(previousPartId, out ITutorialArticle? previousPart) &&
|
|
|
|
previousPart.Visibility == Visibility.Published)
|
|
|
|
{
|
|
|
|
<small>Previous Part</small>
|
|
|
|
<p class="lead">
|
|
|
|
<a asp-page="Article" asp-route-slug="@TutorialService.GetFullSlug(previousPart)">
|
|
|
|
@previousPart.Title
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
<div class="col-sm-12 col-md-6" style="text-align: right;">
|
|
|
|
@if (article.NextPart is { } nextPartId &&
|
|
|
|
TutorialService.TryGetArticle(nextPartId, out ITutorialArticle? nextPart) &&
|
|
|
|
nextPart.Visibility == Visibility.Published)
|
|
|
|
{
|
|
|
|
<small>Next Part</small>
|
|
|
|
<p class="lead">
|
|
|
|
<a asp-page="Article" asp-route-slug="@TutorialService.GetFullSlug(nextPart)">
|
|
|
|
@nextPart.Title
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
}
|
|
|
|
</div>
|
2024-02-20 20:36:23 +00:00
|
|
|
</div>
|
2024-04-27 17:00:20 +01:00
|
|
|
}
|
2024-04-27 16:19:06 +01:00
|
|
|
|
|
|
|
<hr/>
|
|
|
|
|
|
|
|
@if (article.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>
|
|
|
|
}
|
2024-05-01 16:47:31 +01:00
|
|
|
|
|
|
|
int commentCount = TutorialService.GetLegacyCommentCount(article);
|
|
|
|
if (commentCount > 0)
|
|
|
|
{
|
|
|
|
<hr>
|
|
|
|
|
|
|
|
var nestLevelMap = new Dictionary<ILegacyComment, int>();
|
|
|
|
IReadOnlyList<ILegacyComment> legacyComments = TutorialService.GetLegacyComments(article);
|
|
|
|
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 TutorialService.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>
|
|
|
|
}
|
|
|
|
}
|
2024-04-27 16:19:06 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
<p class="text-center text-muted">Comments are not enabled for this post.</p>
|
|
|
|
}
|