Compare commits
2 Commits
e64d8b47b8
...
95dd7e51e5
Author | SHA1 | Date | |
---|---|---|---|
95dd7e51e5 | |||
d11e3f616b |
10
Gulpfile.js
10
Gulpfile.js
@ -4,6 +4,7 @@ const cleanCSS = require('gulp-clean-css');
|
|||||||
const rename = require('gulp-rename');
|
const rename = require('gulp-rename');
|
||||||
const ts = require('gulp-typescript');
|
const ts = require('gulp-typescript');
|
||||||
const terser = require('gulp-terser');
|
const terser = require('gulp-terser');
|
||||||
|
const webpack = require('webpack-stream');
|
||||||
|
|
||||||
const srcDir = 'src';
|
const srcDir = 'src';
|
||||||
const destDir = 'OliverBooth/wwwroot';
|
const destDir = 'OliverBooth/wwwroot';
|
||||||
@ -17,10 +18,13 @@ function compileSCSS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compileTS() {
|
function compileTS() {
|
||||||
return gulp.src(`${srcDir}/ts/**/*.ts`)
|
gulp.src(`${srcDir}/ts/**/*.ts`)
|
||||||
.pipe(ts())
|
.pipe(ts("tsconfig.json"))
|
||||||
.pipe(terser())
|
.pipe(terser())
|
||||||
.pipe(rename({ suffix: '.min' }))
|
.pipe(gulp.dest(`tmp/js`));
|
||||||
|
|
||||||
|
return gulp.src('tmp/js/*.js')
|
||||||
|
.pipe(webpack({ mode: 'production', output: { filename: 'app.min.js' } }))
|
||||||
.pipe(gulp.dest(`${destDir}/js`));
|
.pipe(gulp.dest(`${destDir}/js`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ts", "ts", "{BB9F76AC-292A-
|
|||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
src\ts\app.ts = src\ts\app.ts
|
src\ts\app.ts = src\ts\app.ts
|
||||||
src\ts\prism.js = src\ts\prism.js
|
src\ts\prism.js = src\ts\prism.js
|
||||||
|
src\ts\API.ts = src\ts\API.ts
|
||||||
|
src\ts\BlogPost.ts = src\ts\BlogPost.ts
|
||||||
|
src\ts\Author.ts = src\ts\Author.ts
|
||||||
|
src\ts\TimeUtility.ts = src\ts\TimeUtility.ts
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
70
OliverBooth/Controllers/BlogApiController.cs
Normal file
70
OliverBooth/Controllers/BlogApiController.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
|
using OliverBooth.Services;
|
||||||
|
|
||||||
|
namespace OliverBooth.Controllers;
|
||||||
|
|
||||||
|
[Controller]
|
||||||
|
[Route("/api/blog")]
|
||||||
|
public sealed class BlogApiController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly BlogService _blogService;
|
||||||
|
|
||||||
|
public BlogApiController(BlogService blogService)
|
||||||
|
{
|
||||||
|
_blogService = blogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("count")]
|
||||||
|
public IActionResult Count()
|
||||||
|
{
|
||||||
|
return new JsonResult(new { count = _blogService.AllPosts.Count });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("all/{skip:int?}/{take:int?}")]
|
||||||
|
public IActionResult GetAllBlogPosts(int skip = 0, int take = -1)
|
||||||
|
{
|
||||||
|
if (take == -1) take = _blogService.AllPosts.Count;
|
||||||
|
|
||||||
|
var referer = Request.Headers["Referer"].ToString();
|
||||||
|
Console.WriteLine($"Referer: {referer}");
|
||||||
|
if (!referer.StartsWith(Url.PageLink("/Blog/Index")!))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResult(_blogService.AllPosts.Skip(skip).Take(take).Select(post => new
|
||||||
|
{
|
||||||
|
id = post.Id,
|
||||||
|
commentsEnabled = post.EnableComments,
|
||||||
|
identifier = post.GetDisqusIdentifier(),
|
||||||
|
author = post.AuthorId,
|
||||||
|
title = post.Title,
|
||||||
|
published = post.Published.ToUnixTimeSeconds(),
|
||||||
|
updated = post.Updated?.ToUnixTimeSeconds(),
|
||||||
|
excerpt = _blogService.GetExcerpt(post, out bool trimmed),
|
||||||
|
trimmed,
|
||||||
|
url = Url.Page("/Blog/Article",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
year = post.Published.Year,
|
||||||
|
month = post.Published.Month,
|
||||||
|
day = post.Published.Day,
|
||||||
|
slug = post.Slug
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("author/{id:int}")]
|
||||||
|
public IActionResult GetAuthor(int id)
|
||||||
|
{
|
||||||
|
if (!_blogService.TryGetAuthor(id, out Author? author)) return NotFound();
|
||||||
|
|
||||||
|
return new JsonResult(new
|
||||||
|
{
|
||||||
|
name = author.Name,
|
||||||
|
avatarHash = author.AvatarHash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,15 @@
|
|||||||
@model OliverBooth.Pages.Blog.Index
|
@model OliverBooth.Pages.Blog.Index
|
||||||
@inject BlogService BlogService
|
@inject BlogService BlogService
|
||||||
|
|
||||||
@foreach (BlogPost post in BlogService.AllPosts)
|
<div id="all_blog_posts">
|
||||||
|
<div id="blog_loading_spinner" class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border text-light" role="status">
|
||||||
|
<p class="text-center sr-only">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (BlogPost post in ArraySegment<BlogPost>.Empty /*BlogService.AllPosts*/)
|
||||||
{
|
{
|
||||||
BlogService.TryGetAuthor(post, out Author? author);
|
BlogService.TryGetAuthor(post, out Author? author);
|
||||||
DateTimeOffset published = post.Published;
|
DateTimeOffset published = post.Published;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.1/css/bootstrap.min.css" integrity="sha512-Z/def5z5u2aR89OuzYcxmDJ0Bnd5V1cKqBEbvLOiUNWdg9PQeXVvXLI90SE4QOHGlfLqUnDNVAYyZi8UwUTmWQ==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.1/css/bootstrap.min.css" integrity="sha512-Z/def5z5u2aR89OuzYcxmDJ0Bnd5V1cKqBEbvLOiUNWdg9PQeXVvXLI90SE4QOHGlfLqUnDNVAYyZi8UwUTmWQ==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.css" integrity="sha512-7nTa5CnxbzfQgjQrNmHXB7bxGTUVO/DcYX6rpgt06MkzM0rVXP3EYCv/Ojxg5H0dKbY7llbbYaqgfZjnGOAWGA==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.css" integrity="sha512-7nTa5CnxbzfQgjQrNmHXB7bxGTUVO/DcYX6rpgt06MkzM0rVXP3EYCv/Ojxg5H0dKbY7llbbYaqgfZjnGOAWGA==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" integrity="sha512-c42qTSw/wPZ3/5LBzD+Bw5f7bSF2oxou6wEb+I/lqeaKV5FDIfMvvRp772y4jcJLKuGUOpbJMdg/BTl50fJYAw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;400;700&display=swap" rel="stylesheet">
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2020"
|
|
||||||
}
|
|
||||||
}
|
|
813
package-lock.json
generated
813
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,7 @@
|
|||||||
"gulp-terser": "^2.1.0",
|
"gulp-terser": "^2.1.0",
|
||||||
"gulp-typescript": "^6.0.0-alpha.1",
|
"gulp-typescript": "^6.0.0-alpha.1",
|
||||||
"node-sass": "^9.0.0",
|
"node-sass": "^9.0.0",
|
||||||
"terser": "^5.19.2"
|
"terser": "^5.19.2",
|
||||||
|
"webpack-stream": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,3 +211,28 @@ code[class*="language-"] {
|
|||||||
div.alert *:last-child {
|
div.alert *:last-child {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#blog_loading_spinner {
|
||||||
|
margin: 20px;
|
||||||
|
|
||||||
|
&.removed {
|
||||||
|
animation: spinner-removed 2s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner-removed {
|
||||||
|
0% {
|
||||||
|
transform: scaleY(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scaleY(1);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scaleY(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
27
src/ts/API.ts
Normal file
27
src/ts/API.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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<number> {
|
||||||
|
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/count`);
|
||||||
|
const text = await response.text();
|
||||||
|
return JSON.parse(text).count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getBlogPosts(skip: number, take: number): Promise<BlogPost[]> {
|
||||||
|
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/all/${skip}/${take}`);
|
||||||
|
const text = await response.text();
|
||||||
|
return JSON.parse(text).map(obj => new BlogPost(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAuthor(id: number): Promise<Author> {
|
||||||
|
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/author/${id}`);
|
||||||
|
const text = await response.text();
|
||||||
|
return new Author(JSON.parse(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default API;
|
18
src/ts/Author.ts
Normal file
18
src/ts/Author.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
class Author {
|
||||||
|
private _name: string;
|
||||||
|
private _avatarHash: string;
|
||||||
|
|
||||||
|
constructor(json: any) {
|
||||||
|
this._name = json.name;
|
||||||
|
this._avatarHash = json.avatarHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarHash(): string {
|
||||||
|
return this._avatarHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Author;
|
67
src/ts/BlogPost.ts
Normal file
67
src/ts/BlogPost.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
class BlogPost {
|
||||||
|
private readonly _id: number;
|
||||||
|
private readonly _commentsEnabled: boolean;
|
||||||
|
private readonly _title: string;
|
||||||
|
private readonly _excerpt: string;
|
||||||
|
private readonly _authorId: number;
|
||||||
|
private readonly _published: Date;
|
||||||
|
private readonly _updated?: Date;
|
||||||
|
private readonly _url: string;
|
||||||
|
private readonly _trimmed: boolean;
|
||||||
|
private readonly _identifier: string;
|
||||||
|
|
||||||
|
constructor(json: any) {
|
||||||
|
this._id = json.id;
|
||||||
|
this._commentsEnabled = json.commentsEnabled;
|
||||||
|
this._title = json.title;
|
||||||
|
this._excerpt = json.excerpt;
|
||||||
|
this._authorId = parseInt(json.author);
|
||||||
|
this._published = new Date(json.published * 1000);
|
||||||
|
this._updated = (json.updated && new Date(json.updated * 1000)) || null;
|
||||||
|
this._url = json.url;
|
||||||
|
this._trimmed = json.trimmed;
|
||||||
|
this._identifier = json.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): number {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get commentsEnabled(): boolean {
|
||||||
|
return this._commentsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title(): string {
|
||||||
|
return this._title;
|
||||||
|
}
|
||||||
|
|
||||||
|
get excerpt(): string {
|
||||||
|
return this._excerpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get authorId(): number {
|
||||||
|
return this._authorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get published(): Date {
|
||||||
|
return this._published;
|
||||||
|
}
|
||||||
|
|
||||||
|
get updated(): Date {
|
||||||
|
return this._updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
get trimmed(): boolean {
|
||||||
|
return this._trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get identifier(): string {
|
||||||
|
return this._identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogPost;
|
38
src/ts/TimeUtility.ts
Normal file
38
src/ts/TimeUtility.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
class TimeUtility {
|
||||||
|
public static formatRelativeTimestamp(timestamp: Date) {
|
||||||
|
const now = new Date();
|
||||||
|
// @ts-ignore
|
||||||
|
const diff = now - timestamp;
|
||||||
|
const suffix = diff < 0 ? 'from now' : 'ago';
|
||||||
|
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds} second${seconds !== 1 ? 's' : ''} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
if (minutes < 60) {
|
||||||
|
return `${minutes} minute${minutes !== 1 ? 's' : ''} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
if (hours < 24) {
|
||||||
|
return `${hours} hour${hours !== 1 ? 's' : ''} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(diff / 86400000);
|
||||||
|
if (days < 30) {
|
||||||
|
return `${days} day${days !== 1 ? 's' : ''} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const months = Math.floor(diff / 2592000000);
|
||||||
|
if (months < 12) {
|
||||||
|
return `${months} month${months !== 1 ? 's' : ''} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const years = Math.floor(diff / 31536000000);
|
||||||
|
return `${years} year${years !== 1 ? 's' : ''} ${suffix}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeUtility;
|
@ -1,7 +1,102 @@
|
|||||||
|
import API from "./API";
|
||||||
|
import TimeUtility from "./TimeUtility";
|
||||||
|
|
||||||
declare const bootstrap: any;
|
declare const bootstrap: any;
|
||||||
declare const katex: any;
|
declare const katex: any;
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
const blogPostContainer = document.querySelector("#all_blog_posts");
|
||||||
|
if (blogPostContainer) {
|
||||||
|
API.getBlogPostCount().then(async (count) => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
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 cardBody = document.createElement("div");
|
||||||
|
cardBody.classList.add("card-body");
|
||||||
|
card.appendChild(cardBody);
|
||||||
|
|
||||||
|
const postTitle = document.createElement("h2");
|
||||||
|
postTitle.classList.add("card-title");
|
||||||
|
cardBody.appendChild(postTitle);
|
||||||
|
|
||||||
|
const titleLink = document.createElement("a");
|
||||||
|
titleLink.href = post.url;
|
||||||
|
titleLink.innerText = post.title;
|
||||||
|
postTitle.appendChild(titleLink);
|
||||||
|
|
||||||
|
const metadata = document.createElement("p");
|
||||||
|
metadata.classList.add("text-muted");
|
||||||
|
cardBody.appendChild(metadata);
|
||||||
|
|
||||||
|
const authorIcon = document.createElement("img");
|
||||||
|
authorIcon.classList.add("blog-author-icon");
|
||||||
|
authorIcon.src = `https://gravatar.com/avatar/${author.avatarHash}?s=28`;
|
||||||
|
authorIcon.alt = author.name;
|
||||||
|
metadata.appendChild(authorIcon);
|
||||||
|
|
||||||
|
const authorName = document.createElement("span");
|
||||||
|
authorName.innerHTML = ` ${author.name} • `;
|
||||||
|
metadata.appendChild(authorName);
|
||||||
|
|
||||||
|
const postDate = document.createElement("span");
|
||||||
|
if (post.updated) {
|
||||||
|
postDate.innerHTML = `Updated ${TimeUtility.formatRelativeTimestamp(post.updated)}`;
|
||||||
|
} else {
|
||||||
|
postDate.innerHTML = `Published ${TimeUtility.formatRelativeTimestamp(post.published)}`;
|
||||||
|
}
|
||||||
|
metadata.appendChild(postDate);
|
||||||
|
|
||||||
|
if (post.commentsEnabled) {
|
||||||
|
const bullet = document.createElement("span");
|
||||||
|
bullet.innerHTML = " • ";
|
||||||
|
metadata.appendChild(bullet);
|
||||||
|
|
||||||
|
const commentCount = document.createElement("a");
|
||||||
|
commentCount.href = post.url + "#disqus_thread";
|
||||||
|
commentCount.innerHTML = "0 Comments";
|
||||||
|
commentCount.setAttribute("data-disqus-identifier", post.identifier);
|
||||||
|
metadata.appendChild(commentCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postExcerpt = document.createElement("p");
|
||||||
|
postExcerpt.innerHTML = post.excerpt;
|
||||||
|
cardBody.appendChild(postExcerpt);
|
||||||
|
|
||||||
|
if (post.trimmed) {
|
||||||
|
const readMoreLink = document.createElement("a");
|
||||||
|
readMoreLink.href = post.url;
|
||||||
|
readMoreLink.innerHTML = "Read more …";
|
||||||
|
cardBody.appendChild(readMoreLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
blogPostContainer.appendChild(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;
|
||||||
|
|
||||||
|
const spinner = document.querySelector("#blog_loading_spinner");
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.add("removed");
|
||||||
|
setTimeout(() => spinner.remove(), 1100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const formatRelativeTime = function (timestamp) {
|
const formatRelativeTime = function (timestamp) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"target": "ES2020"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user