Compare commits
15 Commits
221a6f0007
...
e939174040
Author | SHA1 | Date | |
---|---|---|---|
e939174040 | |||
b4c991e44f | |||
ab5277bacb | |||
5bb6463a4b | |||
4244f9f014 | |||
8a3061f23a | |||
506347ce9c | |||
d67955f28a | |||
94a1ee00e1 | |||
28f310e315 | |||
f3ad04ff1f | |||
42af5ebcdd | |||
522caa6add | |||
350247806d | |||
cdc6b1df84 |
@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
README.md = README.md
|
||||
package.json = package.json
|
||||
Gulpfile.js = Gulpfile.js
|
||||
tsconfig.json = tsconfig.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8A323E64-E41E-4780-99FD-17BF58961FB5}"
|
||||
|
@ -43,6 +43,7 @@ public sealed class BlogApiController : ControllerBase
|
||||
author = post.AuthorId,
|
||||
title = post.Title,
|
||||
published = post.Published.ToUnixTimeSeconds(),
|
||||
formattedDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"),
|
||||
updated = post.Updated?.ToUnixTimeSeconds(),
|
||||
humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
|
||||
excerpt = _blogService.GetExcerpt(post, out bool trimmed),
|
||||
|
@ -29,14 +29,14 @@
|
||||
<img class="blog-author-icon" src="https://gravatar.com/avatar/@author.AvatarHash?s=28" alt="@author.Name">
|
||||
@author.Name •
|
||||
|
||||
<abbr data-bs-toggle="tooltip" data-bs-title="@published.ToString("f")">
|
||||
<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>•</span>
|
||||
<abbr data-bs-toggle="tooltip" data-bs-title="@updated.ToString("f")">
|
||||
<abbr data-bs-toggle="tooltip" data-bs-title="@updated.ToString("dddd, d MMMM yyyy HH:mm")">
|
||||
Updated @updated.Humanize()
|
||||
</abbr>
|
||||
}
|
||||
|
@ -19,11 +19,11 @@
|
||||
<img class="blog-author-icon" src="{{author.avatar}}" alt="{{author.name}}">
|
||||
<span>{{author.name}}<span>
|
||||
<span> • </span>
|
||||
<span title="{{ post.date }}">{{ post.date_humanized }}</span>
|
||||
<abbr title="{{ post.formattedDate }}">{{ post.date_humanized }}</abbr>
|
||||
{{#if post.enable_comments}}
|
||||
<span> • </span>
|
||||
<a href="{{post.url}}#disqus_thread" data-disqus-identifier="{{post.disqus_identifier}}">
|
||||
0 Comments
|
||||
Loading comment count …
|
||||
</a>
|
||||
{{/if}}
|
||||
</p>
|
||||
@ -38,4 +38,4 @@
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</script>
|
||||
</script>
|
||||
|
@ -71,7 +71,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.js" integrity="sha512-aoZChv+8imY/U1O7KIHXvO87EOzCuKO0GhFtpD6G2Cyjo/xPeTgdf3/bchB10iB+AojMTDkMHDPLKNxPJVqDcw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js" integrity="sha512-uKQ39gEGiyUJl4AI6L+ekBdGKpGw4xJ55+xyJG7YFlJokPNYegn9KwQ3P8A7aFQAUtUsAQHep+d/lrGqrbPIDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js" integrity="sha512-E1dSFxg+wsfJ4HKjutk/WaCzK7S2wv1POn1RRPGh8ZK+ag9l244Vqxji3r6wgz9YBf6+vhQEYJZpSjqWFPg9gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="~/js/prism.min.js" asp-append-version="true"></script>
|
||||
<script src="~/js/prism.min.js" asp-append-version="true" data-manual></script>
|
||||
<script src="~/js/app.min.js" asp-append-version="true"></script>
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
|
@ -10,6 +10,7 @@
|
||||
private readonly _trimmed: boolean;
|
||||
private readonly _identifier: string;
|
||||
private readonly _humanizedTimestamp: string;
|
||||
private readonly _formattedDate: string;
|
||||
|
||||
constructor(json: any) {
|
||||
this._id = json.id;
|
||||
@ -23,6 +24,7 @@
|
||||
this._trimmed = json.trimmed;
|
||||
this._identifier = json.identifier;
|
||||
this._humanizedTimestamp = json.humanizedTimestamp;
|
||||
this._formattedDate = json.formattedDate;
|
||||
}
|
||||
|
||||
get id(): number {
|
||||
@ -68,6 +70,10 @@
|
||||
get humanizedTimestamp(): string {
|
||||
return this._humanizedTimestamp;
|
||||
}
|
||||
|
||||
get formattedDate(): string {
|
||||
return this._formattedDate;
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogPost;
|
194
src/ts/UI.ts
194
src/ts/UI.ts
@ -1,4 +1,12 @@
|
||||
class UI {
|
||||
import BlogPost from "./BlogPost";
|
||||
import Author from "./Author";
|
||||
import TimeUtility from "./TimeUtility";
|
||||
|
||||
declare const bootstrap: any;
|
||||
declare const katex: any;
|
||||
declare const Prism: any;
|
||||
|
||||
class UI {
|
||||
public static get blogPostContainer(): HTMLDivElement {
|
||||
return document.querySelector("#all-blog-posts");
|
||||
}
|
||||
@ -6,6 +14,190 @@
|
||||
public static get blogPostTemplate(): HTMLDivElement {
|
||||
return document.querySelector("#blog-post-template");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a <script> element that loads the Disqus comment counter.
|
||||
*/
|
||||
public static createDisqusCounterScript(): HTMLScriptElement {
|
||||
const script = document.createElement("script");
|
||||
script.id = "dsq-count-scr";
|
||||
script.src = "https://oliverbooth-dev.disqus.com/count.js";
|
||||
script.async = true;
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blog post card from the given template, post, and author.
|
||||
* @param template The Handlebars template to use.
|
||||
* @param post The blog post to use.
|
||||
* @param author The author of the blog post.
|
||||
*/
|
||||
public static createBlogPostCard(template: any, post: BlogPost, author: Author): HTMLDivElement {
|
||||
const card = document.createElement("div") as HTMLDivElement;
|
||||
card.classList.add("card");
|
||||
card.classList.add("blog-card");
|
||||
card.classList.add("animate__animated");
|
||||
card.classList.add("animate__fadeIn");
|
||||
card.style.marginBottom = "50px";
|
||||
|
||||
const body = template({
|
||||
post: {
|
||||
title: post.title,
|
||||
excerpt: post.excerpt,
|
||||
url: post.url,
|
||||
date: TimeUtility.formatRelativeTimestamp(post.published),
|
||||
formattedDate: post.formattedDate,
|
||||
date_humanized: `${post.updated ? "Updated" : "Published"} ${post.humanizedTimestamp}`,
|
||||
enable_comments: post.commentsEnabled,
|
||||
disqus_identifier: post.identifier,
|
||||
trimmed: post.trimmed,
|
||||
},
|
||||
author: {
|
||||
name: author.name,
|
||||
avatar: `https://gravatar.com/avatar/${author.avatarHash}?s=28`,
|
||||
}
|
||||
});
|
||||
card.innerHTML = body.trim();
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces all UI elements under the given element to update their rendering.
|
||||
* @param element The element to search for UI elements in.
|
||||
*/
|
||||
public static updateUI(element?: Element) {
|
||||
element = element || document.body;
|
||||
UI.addLineNumbers(element);
|
||||
UI.addHighlighting(element);
|
||||
UI.addBootstrapTooltips(element);
|
||||
UI.renderTeX(element);
|
||||
UI.renderTimestamps(element);
|
||||
UI.unescapeMarkTags(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Bootstrap tooltips to all elements with a title attribute.
|
||||
* @param element The element to search for elements with a title attribute in.
|
||||
*/
|
||||
public static addBootstrapTooltips(element?: Element) {
|
||||
element = element || document.body;
|
||||
element.querySelectorAll("[title]").forEach((el) => {
|
||||
el.setAttribute("data-bs-toggle", "tooltip");
|
||||
el.setAttribute("data-bs-placement", "bottom");
|
||||
el.setAttribute("data-bs-html", "true");
|
||||
el.setAttribute("data-bs-title", el.getAttribute("title"));
|
||||
|
||||
new bootstrap.Tooltip(el);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds line numbers to all <pre> <code> blocks that have more than one line.
|
||||
* @param element The element to search for <pre> <code> blocks in.
|
||||
*/
|
||||
public static addLineNumbers(element?: Element) {
|
||||
element = element || document.body;
|
||||
element.querySelectorAll("pre code").forEach((block) => {
|
||||
let content = block.textContent;
|
||||
if (content.trim().split("\n").length > 1) {
|
||||
block.parentElement.classList.add("line-numbers");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds syntax highlighting to all <pre> <code> blocks in the element.
|
||||
* @param element The element to search for <pre> <code> blocks in.
|
||||
*/
|
||||
public static addHighlighting(element?: Element) {
|
||||
element = element || document.body;
|
||||
element.querySelectorAll("pre code").forEach((block) => {
|
||||
Prism.highlightAllUnder(block.parentElement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders all TeX in the document.
|
||||
* @param element The element to search for TeX in.
|
||||
*/
|
||||
public static renderTeX(element?: Element) {
|
||||
element = element || document.body;
|
||||
const tex = element.getElementsByClassName("math");
|
||||
Array.from(tex).forEach(function (el: Element) {
|
||||
let content = el.textContent.trim();
|
||||
if (content.startsWith("\\[")) content = content.slice(2);
|
||||
if (content.endsWith("\\]")) content = content.slice(0, -2);
|
||||
|
||||
katex.render(content, el);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders Discord-style <t:timestamp:format> tags.
|
||||
* @param element The element to search for timestamps in.
|
||||
*/
|
||||
public static renderTimestamps(element?: Element) {
|
||||
element = element || document.body;
|
||||
const timestamps = element.querySelectorAll("span[data-timestamp][data-format]");
|
||||
timestamps.forEach((timestamp) => {
|
||||
const seconds = parseInt(timestamp.getAttribute("data-timestamp"));
|
||||
const format = timestamp.getAttribute("data-format");
|
||||
const date = new Date(seconds * 1000);
|
||||
|
||||
const shortTimeString = date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
const shortDateString = date.toLocaleDateString([], {day: "2-digit", month: "2-digit", year: "numeric"});
|
||||
const longTimeString = date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
const longDateString = date.toLocaleDateString([], {day: "numeric", month: "long", year: "numeric"});
|
||||
const weekday = date.toLocaleString([], {weekday: "long"});
|
||||
timestamp.setAttribute("title", `${weekday}, ${longDateString} ${shortTimeString}`);
|
||||
|
||||
switch (format) {
|
||||
case "t":
|
||||
timestamp.textContent = shortTimeString;
|
||||
break;
|
||||
|
||||
case "T":
|
||||
timestamp.textContent = longTimeString;
|
||||
break;
|
||||
|
||||
case "d":
|
||||
timestamp.textContent = shortDateString;
|
||||
break;
|
||||
|
||||
case "D":
|
||||
timestamp.textContent = longDateString;
|
||||
break;
|
||||
|
||||
case "f":
|
||||
timestamp.textContent = `${longDateString} at ${shortTimeString}`
|
||||
break;
|
||||
|
||||
case "F":
|
||||
timestamp.textContent = `${weekday}, ${longDateString} at ${shortTimeString}`
|
||||
break;
|
||||
|
||||
case "R":
|
||||
setInterval(() => {
|
||||
timestamp.textContent = TimeUtility.formatRelativeTimestamp(date);
|
||||
}, 1000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes all <mark> tags in <pre> <code> blocks.
|
||||
* @param element The element to search for <pre> <code> blocks in.
|
||||
*/
|
||||
public static unescapeMarkTags(element?: Element) {
|
||||
element = element || document.body;
|
||||
element.querySelectorAll("pre code").forEach((block) => {
|
||||
let content = block.innerHTML;
|
||||
content = content.replaceAll("<mark>", "<mark>");
|
||||
content = content.replaceAll("</mark>", "</mark>");
|
||||
block.innerHTML = content;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default UI;
|
117
src/ts/app.ts
117
src/ts/app.ts
@ -1,9 +1,6 @@
|
||||
import API from "./API";
|
||||
import TimeUtility from "./TimeUtility";
|
||||
import API from "./API";
|
||||
import UI from "./UI";
|
||||
|
||||
declare const bootstrap: any;
|
||||
declare const katex: any;
|
||||
declare const Handlebars: any;
|
||||
|
||||
(() => {
|
||||
@ -15,42 +12,15 @@ declare const Handlebars: any;
|
||||
const posts = await API.getBlogPosts(i, 5);
|
||||
for (const post of posts) {
|
||||
const author = await API.getAuthor(post.authorId);
|
||||
|
||||
const card = document.createElement("div") as HTMLDivElement;
|
||||
card.classList.add("card");
|
||||
card.classList.add("blog-card");
|
||||
card.classList.add("animate__animated");
|
||||
card.classList.add("animate__fadeIn");
|
||||
card.style.marginBottom = "50px";
|
||||
|
||||
const body = template({
|
||||
post: {
|
||||
title: post.title,
|
||||
excerpt: post.excerpt,
|
||||
url: post.url,
|
||||
date: TimeUtility.formatRelativeTimestamp(post.published),
|
||||
date_humanized: `${post.updated ? "Updated" : "Published"} ${post.humanizedTimestamp}`,
|
||||
enable_comments: post.commentsEnabled,
|
||||
disqus_identifier: post.identifier,
|
||||
trimmed: post.trimmed,
|
||||
},
|
||||
author: {
|
||||
name: author.name,
|
||||
avatar: `https://gravatar.com/avatar/${author.avatarHash}?s=28`,
|
||||
}
|
||||
});
|
||||
card.innerHTML = body.trim();
|
||||
|
||||
const card = UI.createBlogPostCard(template, post, author);
|
||||
blogPostContainer.appendChild(card);
|
||||
UI.updateUI(card);
|
||||
}
|
||||
|
||||
i += 4;
|
||||
}
|
||||
|
||||
const disqusCounter = document.createElement("script");
|
||||
disqusCounter.id = "dsq-count-scr";
|
||||
disqusCounter.src = "https://oliverbooth-dev.disqus.com/count.js";
|
||||
disqusCounter.async = true;
|
||||
document.body.appendChild(UI.createDisqusCounterScript());
|
||||
|
||||
const spinner = document.querySelector("#blog-loading-spinner");
|
||||
if (spinner) {
|
||||
@ -60,82 +30,5 @@ declare const Handlebars: any;
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
let content = block.textContent;
|
||||
if (content.trim().split("\n").length > 1) {
|
||||
block.parentElement.classList.add("line-numbers");
|
||||
}
|
||||
|
||||
content = block.innerHTML;
|
||||
// @ts-ignore
|
||||
content = content.replaceAll("<mark>", "<mark>");
|
||||
// @ts-ignore
|
||||
content = content.replaceAll("</mark>", "</mark>");
|
||||
block.innerHTML = content;
|
||||
});
|
||||
|
||||
const tex = document.getElementsByClassName("math");
|
||||
Array.prototype.forEach.call(tex, function (el) {
|
||||
let content = el.textContent.trim();
|
||||
if (content.startsWith("\\[")) content = content.slice(2);
|
||||
if (content.endsWith("\\]")) content = content.slice(0, -2);
|
||||
|
||||
katex.render(content, el);
|
||||
});
|
||||
|
||||
const timestamps = document.querySelectorAll("span[data-timestamp][data-format]");
|
||||
timestamps.forEach((timestamp) => {
|
||||
const seconds = parseInt(timestamp.getAttribute("data-timestamp"));
|
||||
const format = timestamp.getAttribute("data-format");
|
||||
const date = new Date(seconds * 1000);
|
||||
|
||||
const shortTimeString = date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
const shortDateString = date.toLocaleDateString([], {day: "2-digit", month: "2-digit", year: "numeric"});
|
||||
const longTimeString = date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
const longDateString = date.toLocaleDateString([], {day: "numeric", month: "long", year: "numeric"});
|
||||
const weekday = date.toLocaleString([], {weekday: "long"});
|
||||
timestamp.setAttribute("title", `${weekday}, ${longDateString} ${shortTimeString}`);
|
||||
|
||||
switch (format) {
|
||||
case "t":
|
||||
timestamp.textContent = shortTimeString;
|
||||
break;
|
||||
|
||||
case "T":
|
||||
timestamp.textContent = longTimeString;
|
||||
break;
|
||||
|
||||
case "d":
|
||||
timestamp.textContent = shortDateString;
|
||||
break;
|
||||
|
||||
case "D":
|
||||
timestamp.textContent = longDateString;
|
||||
break;
|
||||
|
||||
case "f":
|
||||
timestamp.textContent = `${longDateString} at ${shortTimeString}`
|
||||
break;
|
||||
|
||||
case "F":
|
||||
timestamp.textContent = `${weekday}, ${longDateString} at ${shortTimeString}`
|
||||
break;
|
||||
|
||||
case "R":
|
||||
setInterval(() => {
|
||||
timestamp.textContent = TimeUtility.formatRelativeTimestamp(date);
|
||||
}, 1000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("[title]").forEach((el) => {
|
||||
el.setAttribute("data-bs-toggle", "tooltip");
|
||||
el.setAttribute("data-bs-placement", "bottom");
|
||||
el.setAttribute("data-bs-html", "true");
|
||||
el.setAttribute("data-bs-title", el.getAttribute("title"));
|
||||
});
|
||||
|
||||
const list = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
list.forEach((el: Element) => new bootstrap.Tooltip(el));
|
||||
UI.updateUI();
|
||||
})();
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"target": "ES2020"
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user