From 435a69b27a8cfbc573c49abfb3bda16ff8ae0f98 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 18:13:06 +0100 Subject: [PATCH] perf: add pagination to blog post list removes the need for API controller accessed via JS --- .../Services/IBlogPostService.cs | 13 +- .../Controllers/Blog/BlogApiController.cs | 102 --------- OliverBooth/OliverBooth.csproj | 8 +- OliverBooth/Pages/Blog/Index.cshtml | 44 ++-- OliverBooth/Pages/Blog/Index.cshtml.cs | 2 +- OliverBooth/Pages/Blog/List.cshtml | 23 ++ OliverBooth/Pages/Blog/List.cshtml.cs | 32 +++ .../Pages/Shared/Partials/PageTabsUtility.cs | 205 ++++++++++++++++++ .../Pages/Shared/Partials/_BlogCard.cshtml | 56 +++++ .../Pages/Shared/Partials/_PageTabs.cshtml | 21 ++ OliverBooth/Program.cs | 1 - OliverBooth/Services/BlogPostService.cs | 13 +- src/scss/app.scss | 14 +- src/scss/blog.scss | 49 +++++ src/ts/API.ts | 44 ---- src/ts/Author.ts | 25 --- src/ts/BlogPost.ts | 99 --------- src/ts/BlogUrl.ts | 32 --- src/ts/UI.ts | 49 ----- src/ts/app.ts | 65 ------ 20 files changed, 431 insertions(+), 466 deletions(-) delete mode 100644 OliverBooth/Controllers/Blog/BlogApiController.cs create mode 100644 OliverBooth/Pages/Blog/List.cshtml create mode 100644 OliverBooth/Pages/Blog/List.cshtml.cs create mode 100644 OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs create mode 100644 OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml create mode 100644 OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml create mode 100644 src/scss/blog.scss delete mode 100644 src/ts/API.ts delete mode 100644 src/ts/Author.ts delete mode 100644 src/ts/BlogPost.ts delete mode 100644 src/ts/BlogUrl.ts diff --git a/OliverBooth.Common/Services/IBlogPostService.cs b/OliverBooth.Common/Services/IBlogPostService.cs index 29b13e3..5517539 100644 --- a/OliverBooth.Common/Services/IBlogPostService.cs +++ b/OliverBooth.Common/Services/IBlogPostService.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using OliverBooth.Common.Data; using OliverBooth.Common.Data.Blog; namespace OliverBooth.Common.Services; @@ -22,8 +23,9 @@ public interface IBlogPostService /// /// Returns the total number of blog posts. /// + /// The post visibility filter. /// The total number of blog posts. - int GetBlogPostCount(); + int GetBlogPostCount(Visibility visibility = Visibility.None); /// /// Returns a collection of blog posts from the specified page, optionally limiting the number of posts @@ -62,6 +64,15 @@ public interface IBlogPostService /// The next blog post from the specified blog post. IBlogPost? GetNextPost(IBlogPost blogPost); + /// + /// Returns the number of pages needed to render all blog posts, using the specified as an + /// indicator of how many posts are allowed per page. + /// + /// The page size. Defaults to 10. + /// The post visibility filter. + /// The page count. + int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None); + /// /// Returns the previous blog post from the specified blog post. /// diff --git a/OliverBooth/Controllers/Blog/BlogApiController.cs b/OliverBooth/Controllers/Blog/BlogApiController.cs deleted file mode 100644 index 98674e3..0000000 --- a/OliverBooth/Controllers/Blog/BlogApiController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Humanizer; -using Microsoft.AspNetCore.Mvc; -using OliverBooth.Common.Data.Blog; -using OliverBooth.Common.Services; - -namespace OliverBooth.Controllers.Blog; - -/// -/// Represents a controller for the blog API. -/// -[ApiController] -[Route("api/blog")] -[Produces("application/json")] -public sealed class BlogApiController : ControllerBase -{ - private readonly IBlogPostService _blogPostService; - private readonly IBlogUserService _userService; - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - public BlogApiController(IBlogPostService blogPostService, IBlogUserService userService) - { - _blogPostService = blogPostService; - _userService = userService; - } - - [Route("count")] - public IActionResult Count() - { - return Ok(new { count = _blogPostService.GetBlogPostCount() }); - } - - [HttpGet("posts/{page:int?}")] - public IActionResult GetAllBlogPosts(int page = 0) - { - const int itemsPerPage = 10; - IReadOnlyList allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage); - return Ok(allPosts.Select(post => CreatePostObject(post))); - } - - [HttpGet("posts/tagged/{tag}/{page:int?}")] - public IActionResult GetTaggedBlogPosts(string tag, int page = 0) - { - const int itemsPerPage = 10; - tag = tag.Replace('-', ' ').ToLowerInvariant(); - - IReadOnlyList allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage); - allPosts = allPosts.Where(post => post.Tags.Contains(tag)).ToList(); - return Ok(allPosts.Select(post => CreatePostObject(post))); - } - - [HttpGet("author/{id:guid}")] - public IActionResult GetAuthor(Guid id) - { - if (!_userService.TryGetUser(id, out IUser? author)) return NotFound(); - - return Ok(new - { - id = author.Id, - name = author.DisplayName, - avatarUrl = author.AvatarUrl, - }); - } - - [HttpGet("post/{id:guid?}")] - public IActionResult GetPost(Guid id) - { - if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound(); - return Ok(CreatePostObject(post, true)); - } - - private object CreatePostObject(IBlogPost post, bool includeContent = false) - { - return new - { - id = post.Id, - commentsEnabled = post.EnableComments, - identifier = post.GetDisqusIdentifier(), - author = post.Author.Id, - title = post.Title, - published = post.Published.ToUnixTimeSeconds(), - updated = post.Updated?.ToUnixTimeSeconds(), - formattedPublishDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"), - formattedUpdateDate = post.Updated?.ToString("dddd, d MMMM yyyy HH:mm"), - humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(), - excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed), - content = includeContent ? _blogPostService.RenderPost(post) : null, - trimmed, - tags = post.Tags.Select(t => t.Replace(' ', '-')), - url = new - { - year = post.Published.ToString("yyyy"), - month = post.Published.ToString("MM"), - day = post.Published.ToString("dd"), - slug = post.Slug - } - }; - } -} diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj index b00cc91..521bc81 100644 --- a/OliverBooth/OliverBooth.csproj +++ b/OliverBooth/OliverBooth.csproj @@ -49,7 +49,13 @@ - + + + + + + _PageTabs.cshtml + diff --git a/OliverBooth/Pages/Blog/Index.cshtml b/OliverBooth/Pages/Blog/Index.cshtml index 248464f..7b8220f 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml +++ b/OliverBooth/Pages/Blog/Index.cshtml @@ -1,6 +1,9 @@ @page +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Blog @using OliverBooth.Common.Services @model Index +@inject IBlogPostService BlogPostService @{ ViewData["Title"] = "Blog"; @@ -9,36 +12,15 @@ @await Html.PartialAsync("Partials/_MastodonStatus")
- @await Html.PartialAsync("_LoadingSpinner") + @foreach (IBlogPost post in BlogPostService.GetBlogPosts(0)) + { + @await Html.PartialAsync("Partials/_BlogCard", post) + }
- \ No newline at end of file +@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData) +{ + ["UrlRoot"] = "/blog", + ["Page"] = 0, + ["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published) +}) \ No newline at end of file diff --git a/OliverBooth/Pages/Blog/Index.cshtml.cs b/OliverBooth/Pages/Blog/Index.cshtml.cs index 5fa0b34..378ceb2 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml.cs +++ b/OliverBooth/Pages/Blog/Index.cshtml.cs @@ -36,7 +36,7 @@ public class Index : PageModel return _blogPostService.TryGetPost(wpPostId, out IBlogPost? post) ? RedirectToPost(post) : NotFound(); } - private IActionResult RedirectToPost(IBlogPost post) + private RedirectResult RedirectToPost(IBlogPost post) { var route = new { diff --git a/OliverBooth/Pages/Blog/List.cshtml b/OliverBooth/Pages/Blog/List.cshtml new file mode 100644 index 0000000..61fb3d8 --- /dev/null +++ b/OliverBooth/Pages/Blog/List.cshtml @@ -0,0 +1,23 @@ +@page "/blog/page/{pageNumber:int}" +@model List +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Services + +@inject IBlogPostService BlogPostService + +@await Html.PartialAsync("Partials/_MastodonStatus") + +
+ @foreach (IBlogPost post in BlogPostService.GetBlogPosts(Model.PageNumber)) + { + @await Html.PartialAsync("Partials/_BlogCard", post) + } +
+ +@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData) +{ + ["UrlRoot"] = "/blog", + ["Page"] = Model.PageNumber, + ["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published) +}) \ No newline at end of file diff --git a/OliverBooth/Pages/Blog/List.cshtml.cs b/OliverBooth/Pages/Blog/List.cshtml.cs new file mode 100644 index 0000000..f0aa6dd --- /dev/null +++ b/OliverBooth/Pages/Blog/List.cshtml.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace OliverBooth.Pages.Blog; + +/// +/// Represents a class which defines the model for the /blog/page/# route. +/// +public class List : PageModel +{ + /// + /// Gets the requested page number. + /// + /// The requested page number. + public int PageNumber { get; private set; } + + /// + /// Handles the incoming GET request to the page. + /// + /// The requested page number, starting from 1. + /// + public IActionResult OnGet([FromRoute(Name = "pageNumber")] int page = 1) + { + if (page < 2) + { + return RedirectToPage("Index"); + } + + PageNumber = page; + return Page(); + } +} diff --git a/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs b/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs new file mode 100644 index 0000000..833cd77 --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs @@ -0,0 +1,205 @@ +using Cysharp.Text; +using HtmlAgilityPack; + +namespace OliverBooth.Pages.Shared.Partials; + +/// +/// Provides methods for displaying pagination tabs. +/// +public class PageTabsUtility +{ + private string _urlRoot = string.Empty; + + /// + /// Gets or sets the current page number. + /// + /// The current page number. + public int CurrentPage { get; set; } = 1; + + /// + /// Gets or sets the page count. + /// + /// The page count. + public int PageCount { get; set; } = 1; + + /// + /// Gets or sets the URL root. + /// + /// The URL root. + public string UrlRoot + { + get => _urlRoot; + set => _urlRoot = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + } + + /// + /// Shows the bound chevrons for the specified bounds type. + /// + /// The bounds type to display. + /// An HTML string containing the elements representing the bound chevrons. + public string ShowBounds(BoundsType bounds) + { + return bounds switch + { + BoundsType.Lower => ShowLowerBound(), + BoundsType.Upper => ShowUpperBound(PageCount), + _ => string.Empty + }; + } + + /// + /// Shows the specified page tab. + /// + /// The tab to display. + /// An HTML string containing the element for the specified page tab. + public string ShowTab(int tab) + { + var document = new HtmlDocument(); + HtmlNode listItem = document.CreateElement("li"); + HtmlNode pageLink; + listItem.AddClass("page-item"); + + switch (tab) + { + case 0: + // show ... to indicate truncation + pageLink = document.CreateElement("span"); + pageLink.InnerHtml = "..."; + break; + + case var _ when CurrentPage == tab: + listItem.AddClass("active"); + listItem.SetAttributeValue("aria-current", "page"); + + pageLink = document.CreateElement("span"); + pageLink.InnerHtml = tab.ToString(); + break; + + default: + pageLink = document.CreateElement("a"); + pageLink.SetAttributeValue("href", GetLinkForPage(tab)); + pageLink.InnerHtml = tab.ToString(); + break; + } + + pageLink.AddClass("page-link"); + listItem.AppendChild(pageLink); + + document.DocumentNode.AppendChild(listItem); + return document.DocumentNode.InnerHtml; + } + + /// + /// Shows the paginated tab window. + /// + /// An HTML string representing the page tabs. + public string ShowTabWindow() + { + using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); + + int windowLowerBound = Math.Max(CurrentPage - 2, 1); + int windowUpperBound = Math.Min(CurrentPage + 2, PageCount); + + if (windowLowerBound > 2) + { + // show lower truncation ... + builder.AppendLine(ShowTab(0)); + } + + for (int pageIndex = windowLowerBound; pageIndex <= windowUpperBound; pageIndex++) + { + if (pageIndex == 1 || pageIndex == PageCount) + { + // don't show bounds, these are explicitly written + continue; + } + + builder.AppendLine(ShowTab(pageIndex)); + } + + if (windowUpperBound < PageCount - 1) + { + // show upper truncation ... + builder.AppendLine(ShowTab(0)); + } + + return builder.ToString(); + } + + private string GetLinkForPage(int page) + { + // page 1 doesn't use /page/n route + return page == 1 ? _urlRoot : $"{_urlRoot}/page/{page}"; + } + + private string ShowLowerBound() + { + if (CurrentPage <= 1) + { + return string.Empty; + } + + var document = new HtmlDocument(); + HtmlNode listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + + HtmlNode pageLink = document.CreateElement("a"); + listItem.AppendChild(pageLink); + pageLink.AddClass("page-link"); + pageLink.SetAttributeValue("href", UrlRoot); + pageLink.InnerHtml = "≪"; + + document.DocumentNode.AppendChild(listItem); + + listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + + pageLink = document.CreateElement("a"); + listItem.AppendChild(pageLink); + pageLink.AddClass("page-link"); + pageLink.InnerHtml = "<"; + pageLink.SetAttributeValue("href", GetLinkForPage(CurrentPage - 1)); + + document.DocumentNode.AppendChild(listItem); + + return document.DocumentNode.InnerHtml; + } + + private string ShowUpperBound(int pageCount) + { + if (CurrentPage >= pageCount) + { + return string.Empty; + } + + var document = new HtmlDocument(); + + HtmlNode pageLink = document.CreateElement("a"); + pageLink.AddClass("page-link"); + pageLink.SetAttributeValue("href", GetLinkForPage(CurrentPage + 1)); + pageLink.InnerHtml = ">"; + + HtmlNode listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + listItem.AppendChild(pageLink); + document.DocumentNode.AppendChild(listItem); + + pageLink = document.CreateElement("a"); + pageLink.AddClass("page-link"); + pageLink.SetAttributeValue("href", GetLinkForPage(pageCount)); + pageLink.InnerHtml = "≫"; + + listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + listItem.AppendChild(pageLink); + document.DocumentNode.AppendChild(listItem); + + return document.DocumentNode.InnerHtml; + } + + public enum BoundsType + { + Lower, + Upper + } +} diff --git a/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml b/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml new file mode 100644 index 0000000..29bd396 --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml @@ -0,0 +1,56 @@ +@using Humanizer +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Services +@model IBlogPost +@inject IBlogPostService BlogPostService + +@{ + IBlogAuthor author = Model.Author; + DateTimeOffset published = Model.Published; + DateTimeOffset? updated = Model.Updated; + DateTimeOffset time = updated ?? published; + string verb = updated is null ? "Published" : "Updated"; +} + +
+

+ + @Model.Title + +

+ +

+ @author.DisplayName + @author.DisplayName + • + @verb @time.Humanize() +

+ +
+ @Html.Raw(BlogPostService.RenderExcerpt(Model, out bool trimmed)) +
+ + @if (trimmed || Model.Excerpt is not null) + { +

+ + Read more... + +

+ } + +
+ + @foreach (string tag in Model.Tags) + { + @tag + } +
\ No newline at end of file diff --git a/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml b/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml new file mode 100644 index 0000000..6293a4f --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml @@ -0,0 +1,21 @@ +@{ + var urlRoot = ViewData["UrlRoot"]?.ToString() ?? string.Empty; + var page = (int)(ViewData["Page"] ?? 1); + var pageCount = (int)(ViewData["PageCount"] ?? 1); + + var utility = new PageTabsUtility + { + CurrentPage = page, + PageCount = pageCount, + UrlRoot = urlRoot + }; +} + \ No newline at end of file diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 84203ce..0da5115 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -89,7 +89,6 @@ if (!app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); -app.UseStatusCodePagesWithRedirects("/error/{0}"); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); diff --git a/OliverBooth/Services/BlogPostService.cs b/OliverBooth/Services/BlogPostService.cs index 2190f09..e844024 100644 --- a/OliverBooth/Services/BlogPostService.cs +++ b/OliverBooth/Services/BlogPostService.cs @@ -36,10 +36,12 @@ internal sealed class BlogPostService : IBlogPostService } /// - public int GetBlogPostCount() + public int GetBlogPostCount(Visibility visibility = Visibility.None) { using BlogContext context = _dbContextFactory.CreateDbContext(); - return context.BlogPosts.Count(); + return visibility == Visibility.None + ? context.BlogPosts.Count() + : context.BlogPosts.Count(p => p.Visibility == visibility); } /// @@ -100,6 +102,13 @@ internal sealed class BlogPostService : IBlogPostService .FirstOrDefault(post => post.Published > blogPost.Published); } + /// + public int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None) + { + float postCount = GetBlogPostCount(visibility); + return (int)MathF.Ceiling(postCount / pageSize); + } + /// public IBlogPost? GetPreviousPost(IBlogPost blogPost) { diff --git a/src/scss/app.scss b/src/scss/app.scss index df43c57..e01d695 100644 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -1,4 +1,5 @@ @import "markdown"; +@import "blog"; html, body { background: #121212; @@ -229,19 +230,6 @@ article { } } -.blog-card { - transition: all 0.2s ease-in-out; - - &:hover { - transform: scale(1.05); - } - - article { - background: none; - padding: 0; - } -} - code:not([class*="language-"]) { background: #1e1e1e !important; color: #dcdcda !important; diff --git a/src/scss/blog.scss b/src/scss/blog.scss new file mode 100644 index 0000000..0b76ce6 --- /dev/null +++ b/src/scss/blog.scss @@ -0,0 +1,49 @@ +$blog-card-bg: #333333; +$blog-card-gutter: 20px; +$border-radius: 3px; + +div.blog-card { + background: $blog-card-bg; + margin-bottom: $blog-card-gutter; + padding: $blog-card-gutter; + border-radius: $border-radius; + + :last-child { + margin-bottom: 0; + } + + article { + padding: 0; + margin: 0; + } +} + + +ul.pagination { + border: none; + + li { + a, span { + border-radius: $border-radius !important; + border: none; + + &:link, &:visited, &:active { + color: #007ec6; + } + } + + &.active a, &.active span { + background: #007ec6; + } + + &:not(.active) a, &:not(.active) span { + background: none; + } + + &:hover { + a { + color: #ffffff !important; + } + } + } +} \ No newline at end of file diff --git a/src/ts/API.ts b/src/ts/API.ts deleted file mode 100644 index a1e070d..0000000 --- a/src/ts/API.ts +++ /dev/null @@ -1,44 +0,0 @@ -import BlogPost from "./BlogPost"; -import Author from "./Author"; - -class API { - private static readonly BASE_URL: string = "/api"; - private static readonly BLOG_URL: string = "/blog"; - - static async getBlogPostCount(): Promise { - const response = await API.getResponse(`count`); - return response.count; - } - - static async getBlogPost(id: string): Promise { - const response = await API.getResponse(`post/${id}`); - return new BlogPost(response); - } - - static async getBlogPosts(page: number): Promise { - const response = await API.getResponse(`posts/${page}`); - return response.map(obj => new BlogPost(obj)); - } - - static async getBlogPostsByTag(tag: string, page: number): Promise { - const response = await API.getResponse(`posts/tagged/${tag}/${page}`); - return response.map(obj => new BlogPost(obj)); - } - - static async getAuthor(id: string): Promise { - const response = await API.getResponse(`author/${id}`); - return new Author(response); - } - - private static async getResponse(url: string): Promise { - const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/${url}`); - if (response.status !== 200) { - throw new Error("Invalid response from server"); - } - - const text = await response.text(); - return JSON.parse(text); - } -} - -export default API; \ No newline at end of file diff --git a/src/ts/Author.ts b/src/ts/Author.ts deleted file mode 100644 index 8faf1a5..0000000 --- a/src/ts/Author.ts +++ /dev/null @@ -1,25 +0,0 @@ -class Author { - private readonly _id: string; - private readonly _name: string; - private readonly _avatarUrl: string; - - constructor(json: any) { - this._id = json.id; - this._name = json.name; - this._avatarUrl = json.avatarUrl; - } - - get id(): string { - return this._id; - } - - get name(): string { - return this._name; - } - - get avatarUrl(): string { - return this._avatarUrl; - } -} - -export default Author; \ No newline at end of file diff --git a/src/ts/BlogPost.ts b/src/ts/BlogPost.ts deleted file mode 100644 index 1186964..0000000 --- a/src/ts/BlogPost.ts +++ /dev/null @@ -1,99 +0,0 @@ -import BlogUrl from "./BlogUrl"; - -class BlogPost { - private readonly _id: string; - private readonly _commentsEnabled: boolean; - private readonly _title: string; - private readonly _excerpt: string; - private readonly _content: string; - private readonly _authorId: string; - private readonly _published: Date; - private readonly _updated?: Date; - private readonly _url: BlogUrl; - private readonly _trimmed: boolean; - private readonly _identifier: string; - private readonly _humanizedTimestamp: string; - private readonly _formattedPublishDate: string; - private readonly _formattedUpdateDate: string; - private readonly _tags: string[]; - - constructor(json: any) { - this._id = json.id; - this._commentsEnabled = json.commentsEnabled; - this._title = json.title; - this._excerpt = json.excerpt; - this._content = json.content; - this._authorId = json.author; - this._published = new Date(json.published * 1000); - this._updated = (json.updated && new Date(json.updated * 1000)) || null; - this._url = new BlogUrl(json.url); - this._trimmed = json.trimmed; - this._identifier = json.identifier; - this._humanizedTimestamp = json.humanizedTimestamp; - this._formattedPublishDate = json.formattedPublishDate; - this._formattedUpdateDate = json.formattedUpdateDate; - this._tags = json.tags; - } - - get id(): string { - return this._id; - } - - get commentsEnabled(): boolean { - return this._commentsEnabled; - } - - get title(): string { - return this._title; - } - - get excerpt(): string { - return this._excerpt; - } - - get content(): string { - return this._content; - } - - get authorId(): string { - return this._authorId; - } - - get published(): Date { - return this._published; - } - - get updated(): Date { - return this._updated; - } - - get url(): BlogUrl { - return this._url; - } - - get tags(): string[] { - return this._tags; - } - - get trimmed(): boolean { - return this._trimmed; - } - - get identifier(): string { - return this._identifier; - } - - get humanizedTimestamp(): string { - return this._humanizedTimestamp; - } - - get formattedPublishDate(): string { - return this._formattedPublishDate; - } - - get formattedUpdateDate(): string { - return this._formattedUpdateDate; - } -} - -export default BlogPost; \ No newline at end of file diff --git a/src/ts/BlogUrl.ts b/src/ts/BlogUrl.ts deleted file mode 100644 index 351ad3c..0000000 --- a/src/ts/BlogUrl.ts +++ /dev/null @@ -1,32 +0,0 @@ -class BlogUrl { - private readonly _year: string; - private readonly _month: string; - private readonly _day: string; - private readonly _slug: string; - - constructor(json: any) { - this._year = json.year; - this._month = json.month; - this._day = json.day; - this._slug = json.slug; - } - - - get year(): string { - return this._year; - } - - get month(): string { - return this._month; - } - - get day(): string { - return this._day; - } - - get slug(): string { - return this._slug; - } -} - -export default BlogUrl; \ No newline at end of file diff --git a/src/ts/UI.ts b/src/ts/UI.ts index 3c9d9ed..554eb07 100644 --- a/src/ts/UI.ts +++ b/src/ts/UI.ts @@ -1,5 +1,3 @@ -import BlogPost from "./BlogPost"; -import Author from "./Author"; import TimeUtility from "./TimeUtility"; declare const bootstrap: any; @@ -7,18 +5,6 @@ declare const katex: any; declare const Prism: any; class UI { - public static get blogPost(): HTMLDivElement { - return document.querySelector("article[data-blog-post='true']"); - } - - public static get blogPostContainer(): HTMLDivElement { - return document.querySelector("#all-blog-posts"); - } - - public static get blogPostTemplate(): HTMLDivElement { - return document.querySelector("#blog-post-template"); - } - /** * Creates a