Compare commits
No commits in common. "95dd7e51e5949eb1d21ba5293a7e587938290a3a" and "e64d8b47b8a2cc4323c3330fd448e05315330d37" have entirely different histories.
95dd7e51e5
...
e64d8b47b8
10
Gulpfile.js
10
Gulpfile.js
@ -4,7 +4,6 @@ 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';
|
||||||
@ -18,13 +17,10 @@ function compileSCSS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compileTS() {
|
function compileTS() {
|
||||||
gulp.src(`${srcDir}/ts/**/*.ts`)
|
return gulp.src(`${srcDir}/ts/**/*.ts`)
|
||||||
.pipe(ts("tsconfig.json"))
|
.pipe(ts())
|
||||||
.pipe(terser())
|
.pipe(terser())
|
||||||
.pipe(gulp.dest(`tmp/js`));
|
.pipe(rename({ suffix: '.min' }))
|
||||||
|
|
||||||
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,10 +22,6 @@ 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
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
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,15 +5,7 @@
|
|||||||
@model OliverBooth.Pages.Blog.Index
|
@model OliverBooth.Pages.Blog.Index
|
||||||
@inject BlogService BlogService
|
@inject BlogService BlogService
|
||||||
|
|
||||||
<div id="all_blog_posts">
|
@foreach (BlogPost post in BlogService.AllPosts)
|
||||||
<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,7 +9,6 @@
|
|||||||
<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">
|
||||||
|
5
OliverBooth/tsconfig.json
Normal file
5
OliverBooth/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020"
|
||||||
|
}
|
||||||
|
}
|
813
package-lock.json
generated
813
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,6 @@
|
|||||||
"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,28 +211,3 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +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<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;
|
|
@ -1,18 +0,0 @@
|
|||||||
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;
|
|
@ -1,67 +0,0 @@
|
|||||||
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;
|
|
@ -1,38 +0,0 @@
|
|||||||
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,102 +1,7 @@
|
|||||||
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
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ES2020", "DOM"],
|
|
||||||
"target": "ES2020"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user