Compare commits
12 Commits
20656e74e8
...
221a6f0007
Author | SHA1 | Date | |
---|---|---|---|
221a6f0007 | |||
87c54fa5a4 | |||
217c1d660e | |||
3868fcbaa8 | |||
1de869c6f0 | |||
fa17f63b82 | |||
fb6eabf55f | |||
9ef9e6ca2c | |||
085bdafda2 | |||
f989b4c02f | |||
aa69713e49 | |||
cb331ff54f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ project.lock.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.pyc
|
*.pyc
|
||||||
nupkg/
|
nupkg/
|
||||||
|
tmp/
|
||||||
|
|
||||||
# Visual Studio Code
|
# Visual Studio Code
|
||||||
.vscode
|
.vscode
|
||||||
|
@ -18,11 +18,13 @@ function compileSCSS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compileTS() {
|
function compileTS() {
|
||||||
gulp.src(`${srcDir}/ts/**/*.ts`)
|
return gulp.src(`${srcDir}/ts/**/*.ts`)
|
||||||
.pipe(ts("tsconfig.json"))
|
.pipe(ts("tsconfig.json"))
|
||||||
.pipe(terser())
|
.pipe(terser())
|
||||||
.pipe(gulp.dest(`tmp/js`));
|
.pipe(gulp.dest(`tmp/js`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundleJS() {
|
||||||
return gulp.src('tmp/js/*.js')
|
return gulp.src('tmp/js/*.js')
|
||||||
.pipe(webpack({ mode: 'production', output: { filename: 'app.min.js' } }))
|
.pipe(webpack({ mode: 'production', output: { filename: 'app.min.js' } }))
|
||||||
.pipe(gulp.dest(`${destDir}/js`));
|
.pipe(gulp.dest(`${destDir}/js`));
|
||||||
@ -46,4 +48,4 @@ function copyImages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.default = compileSCSS;
|
exports.default = compileSCSS;
|
||||||
exports.default = gulp.parallel(compileSCSS, compileTS, copyCSS, copyJS, copyImages);
|
exports.default = gulp.parallel(compileSCSS, gulp.series(compileTS, bundleJS), copyCSS, copyJS, copyImages);
|
||||||
|
@ -26,6 +26,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ts", "ts", "{BB9F76AC-292A-
|
|||||||
src\ts\BlogPost.ts = src\ts\BlogPost.ts
|
src\ts\BlogPost.ts = src\ts\BlogPost.ts
|
||||||
src\ts\Author.ts = src\ts\Author.ts
|
src\ts\Author.ts = src\ts\Author.ts
|
||||||
src\ts\TimeUtility.ts = src\ts\TimeUtility.ts
|
src\ts\TimeUtility.ts = src\ts\TimeUtility.ts
|
||||||
|
src\ts\UI.ts = src\ts\UI.ts
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Humanizer;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using OliverBooth.Data.Blog;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
@ -43,6 +44,7 @@ public sealed class BlogApiController : ControllerBase
|
|||||||
title = post.Title,
|
title = post.Title,
|
||||||
published = post.Published.ToUnixTimeSeconds(),
|
published = post.Published.ToUnixTimeSeconds(),
|
||||||
updated = post.Updated?.ToUnixTimeSeconds(),
|
updated = post.Updated?.ToUnixTimeSeconds(),
|
||||||
|
humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
|
||||||
excerpt = _blogService.GetExcerpt(post, out bool trimmed),
|
excerpt = _blogService.GetExcerpt(post, out bool trimmed),
|
||||||
trimmed,
|
trimmed,
|
||||||
url = Url.Page("/Blog/Article",
|
url = Url.Page("/Blog/Article",
|
||||||
|
@ -1,79 +1,41 @@
|
|||||||
@page
|
@page
|
||||||
@using Humanizer
|
|
||||||
@using OliverBooth.Data.Blog
|
|
||||||
@using OliverBooth.Services
|
|
||||||
@model OliverBooth.Pages.Blog.Index
|
@model OliverBooth.Pages.Blog.Index
|
||||||
@inject BlogService BlogService
|
|
||||||
|
|
||||||
<div id="all_blog_posts">
|
<div id="all-blog-posts">
|
||||||
<div id="blog_loading_spinner" class="d-flex justify-content-center">
|
<div id="blog-loading-spinner" class="d-flex justify-content-center">
|
||||||
<div class="spinner-border text-light" role="status">
|
<div class="spinner-border text-light" role="status">
|
||||||
<p class="text-center sr-only">Loading...</p>
|
<p class="text-center sr-only">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@foreach (BlogPost post in ArraySegment<BlogPost>.Empty /*BlogService.AllPosts*/)
|
<script id="blog-post-template" type="text/x-handlebars-template">
|
||||||
{
|
<div class="card-body">
|
||||||
BlogService.TryGetAuthor(post, out Author? author);
|
<h2>
|
||||||
DateTimeOffset published = post.Published;
|
<a href="{{post.url}}"> {{post.title}}</a>
|
||||||
DateTimeOffset timestamp = post.Updated ?? published;
|
</h2>
|
||||||
bool isUpdated = post.Updated.HasValue;
|
|
||||||
var year = published.ToString("yyyy");
|
|
||||||
var month = published.ToString("MM");
|
|
||||||
var day = published.ToString("dd");
|
|
||||||
|
|
||||||
<div class="card blog-card" style="margin-bottom: 50px;">
|
<p class="text-muted">
|
||||||
<div class="card-body">
|
<img class="blog-author-icon" src="{{author.avatar}}" alt="{{author.name}}">
|
||||||
<h2>
|
<span>{{author.name}}<span>
|
||||||
<a asp-page="/blog/article"
|
<span> • </span>
|
||||||
asp-route-year="@year"
|
<span title="{{ post.date }}">{{ post.date_humanized }}</span>
|
||||||
asp-route-month="@month"
|
{{#if post.enable_comments}}
|
||||||
asp-route-day="@day"
|
<span> • </span>
|
||||||
asp-route-slug="@post.Slug">
|
<a href="{{post.url}}#disqus_thread" data-disqus-identifier="{{post.disqus_identifier}}">
|
||||||
@post.Title
|
0 Comments
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p class="text-muted">
|
<p>{{{post.excerpt}}}</p>
|
||||||
<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="@timestamp.ToString("f")">
|
{{#if post.trimmed}}
|
||||||
@(isUpdated ? "Updated" : "Published") @timestamp.Humanize()
|
<p>
|
||||||
</abbr>
|
<a href="{{post.url}}">
|
||||||
|
Read more...
|
||||||
@if (post.EnableComments)
|
</a>
|
||||||
{
|
|
||||||
<span> •</span>
|
|
||||||
<a asp-page="/blog/article"
|
|
||||||
asp-fragment="disqus_thread"
|
|
||||||
asp-route-year="@year"
|
|
||||||
asp-route-month="@month"
|
|
||||||
asp-route-day="@day"
|
|
||||||
asp-route-slug="@post.Slug"
|
|
||||||
data-disqus-identifier="@post.GetDisqusIdentifier()">
|
|
||||||
0 Comments
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
|
{{/if}}
|
||||||
<p>@Html.Raw(BlogService.GetExcerpt(post, out bool trimmed))</p>
|
|
||||||
|
|
||||||
@if (trimmed)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
<a asp-page="/blog/article"
|
|
||||||
asp-route-year="@year"
|
|
||||||
asp-route-month="@month"
|
|
||||||
asp-route-day="@day"
|
|
||||||
asp-route-slug="@post.Slug">
|
|
||||||
Read more...
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</script>
|
||||||
<script id="dsq-count-scr" src="https://oliverbooth-dev.disqus.com/count.js" async></script>
|
|
||||||
}
|
|
@ -70,6 +70,7 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.1/js/bootstrap.bundle.min.js" integrity="sha512-ToL6UYWePxjhDQKNioSi4AyJ5KkRxY+F1+Fi7Jgh0Hp5Kk2/s8FD7zusJDdonfe5B00Qw+B8taXxF6CFLnqNCw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.1/js/bootstrap.bundle.min.js" integrity="sha512-ToL6UYWePxjhDQKNioSi4AyJ5KkRxY+F1+Fi7Jgh0Hp5Kk2/s8FD7zusJDdonfe5B00Qw+B8taXxF6CFLnqNCw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
<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/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/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"></script>
|
||||||
<script src="~/js/app.min.js" asp-append-version="true"></script>
|
<script src="~/js/app.min.js" asp-append-version="true"></script>
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Markdig;
|
using Markdig;
|
||||||
|
using Markdig.Extensions.MediaLinks;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
using OliverBooth.Markdown;
|
using OliverBooth.Markdown;
|
||||||
|
@ -61,7 +61,25 @@ public sealed class BlogService
|
|||||||
int moreIndex = span.IndexOf("<!--more-->", StringComparison.Ordinal);
|
int moreIndex = span.IndexOf("<!--more-->", StringComparison.Ordinal);
|
||||||
trimmed = moreIndex != -1 || span.Length > 256;
|
trimmed = moreIndex != -1 || span.Length > 256;
|
||||||
string result = moreIndex != -1 ? span[..moreIndex].Trim().ToString() : post.Body.Truncate(256);
|
string result = moreIndex != -1 ? span[..moreIndex].Trim().ToString() : post.Body.Truncate(256);
|
||||||
return RenderContent(result);
|
return RenderContent(result).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to find the author by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the author.</param>
|
||||||
|
/// <param name="author">
|
||||||
|
/// When this method returns, contains the <see cref="Author" /> associated with the specified ID, if the author
|
||||||
|
/// is found; otherwise, <see langword="null" />.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><see langword="true" /> if the author is found; otherwise, <see langword="false" />.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="post" /> is <see langword="null" />.</exception>
|
||||||
|
public bool TryGetAuthor(int id, [NotNullWhen(true)] out Author? author)
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
author = context.Authors.FirstOrDefault(a => a.Id == id);
|
||||||
|
|
||||||
|
return author is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -71,6 +89,7 @@ public sealed class BlogService
|
|||||||
/// <param name="author">
|
/// <param name="author">
|
||||||
/// When this method returns, contains the <see cref="Author" /> associated with the specified blog post, if the
|
/// When this method returns, contains the <see cref="Author" /> associated with the specified blog post, if the
|
||||||
/// author is found; otherwise, <see langword="null" />.
|
/// author is found; otherwise, <see langword="null" />.
|
||||||
|
/// </param>
|
||||||
/// <returns><see langword="true" /> if the author is found; otherwise, <see langword="false" />.</returns>
|
/// <returns><see langword="true" /> if the author is found; otherwise, <see langword="false" />.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="post" /> is <see langword="null" />.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="post" /> is <see langword="null" />.</exception>
|
||||||
public bool TryGetAuthor(BlogPost post, [NotNullWhen(true)] out Author? author)
|
public bool TryGetAuthor(BlogPost post, [NotNullWhen(true)] out Author? author)
|
||||||
|
@ -212,7 +212,7 @@ div.alert *:last-child {
|
|||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blog_loading_spinner {
|
#blog-loading-spinner {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|
||||||
&.removed {
|
&.removed {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
class Author {
|
class Author {
|
||||||
private _name: string;
|
private readonly _name: string;
|
||||||
private _avatarHash: string;
|
private readonly _avatarHash: string;
|
||||||
|
|
||||||
constructor(json: any) {
|
constructor(json: any) {
|
||||||
this._name = json.name;
|
this._name = json.name;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
private readonly _url: string;
|
private readonly _url: string;
|
||||||
private readonly _trimmed: boolean;
|
private readonly _trimmed: boolean;
|
||||||
private readonly _identifier: string;
|
private readonly _identifier: string;
|
||||||
|
private readonly _humanizedTimestamp: string;
|
||||||
|
|
||||||
constructor(json: any) {
|
constructor(json: any) {
|
||||||
this._id = json.id;
|
this._id = json.id;
|
||||||
@ -21,6 +22,7 @@
|
|||||||
this._url = json.url;
|
this._url = json.url;
|
||||||
this._trimmed = json.trimmed;
|
this._trimmed = json.trimmed;
|
||||||
this._identifier = json.identifier;
|
this._identifier = json.identifier;
|
||||||
|
this._humanizedTimestamp = json.humanizedTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id(): number {
|
get id(): number {
|
||||||
@ -62,6 +64,10 @@
|
|||||||
get identifier(): string {
|
get identifier(): string {
|
||||||
return this._identifier;
|
return this._identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get humanizedTimestamp(): string {
|
||||||
|
return this._humanizedTimestamp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlogPost;
|
export default BlogPost;
|
11
src/ts/UI.ts
Normal file
11
src/ts/UI.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
class UI {
|
||||||
|
public static get blogPostContainer(): HTMLDivElement {
|
||||||
|
return document.querySelector("#all-blog-posts");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get blogPostTemplate(): HTMLDivElement {
|
||||||
|
return document.querySelector("#blog-post-template");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UI;
|
@ -1,12 +1,15 @@
|
|||||||
import API from "./API";
|
import API from "./API";
|
||||||
import TimeUtility from "./TimeUtility";
|
import TimeUtility from "./TimeUtility";
|
||||||
|
import UI from "./UI";
|
||||||
|
|
||||||
declare const bootstrap: any;
|
declare const bootstrap: any;
|
||||||
declare const katex: any;
|
declare const katex: any;
|
||||||
|
declare const Handlebars: any;
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const blogPostContainer = document.querySelector("#all_blog_posts");
|
const blogPostContainer = UI.blogPostContainer;
|
||||||
if (blogPostContainer) {
|
if (blogPostContainer) {
|
||||||
|
const template = Handlebars.compile(UI.blogPostTemplate.innerHTML);
|
||||||
API.getBlogPostCount().then(async (count) => {
|
API.getBlogPostCount().then(async (count) => {
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const posts = await API.getBlogPosts(i, 5);
|
const posts = await API.getBlogPosts(i, 5);
|
||||||
@ -20,63 +23,23 @@ declare const katex: any;
|
|||||||
card.classList.add("animate__fadeIn");
|
card.classList.add("animate__fadeIn");
|
||||||
card.style.marginBottom = "50px";
|
card.style.marginBottom = "50px";
|
||||||
|
|
||||||
const cardBody = document.createElement("div");
|
const body = template({
|
||||||
cardBody.classList.add("card-body");
|
post: {
|
||||||
card.appendChild(cardBody);
|
title: post.title,
|
||||||
|
excerpt: post.excerpt,
|
||||||
const postTitle = document.createElement("h2");
|
url: post.url,
|
||||||
postTitle.classList.add("card-title");
|
date: TimeUtility.formatRelativeTimestamp(post.published),
|
||||||
cardBody.appendChild(postTitle);
|
date_humanized: `${post.updated ? "Updated" : "Published"} ${post.humanizedTimestamp}`,
|
||||||
|
enable_comments: post.commentsEnabled,
|
||||||
const titleLink = document.createElement("a");
|
disqus_identifier: post.identifier,
|
||||||
titleLink.href = post.url;
|
trimmed: post.trimmed,
|
||||||
titleLink.innerText = post.title;
|
},
|
||||||
postTitle.appendChild(titleLink);
|
author: {
|
||||||
|
name: author.name,
|
||||||
const metadata = document.createElement("p");
|
avatar: `https://gravatar.com/avatar/${author.avatarHash}?s=28`,
|
||||||
metadata.classList.add("text-muted");
|
}
|
||||||
cardBody.appendChild(metadata);
|
});
|
||||||
|
card.innerHTML = body.trim();
|
||||||
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);
|
blogPostContainer.appendChild(card);
|
||||||
}
|
}
|
||||||
@ -89,7 +52,7 @@ declare const katex: any;
|
|||||||
disqusCounter.src = "https://oliverbooth-dev.disqus.com/count.js";
|
disqusCounter.src = "https://oliverbooth-dev.disqus.com/count.js";
|
||||||
disqusCounter.async = true;
|
disqusCounter.async = true;
|
||||||
|
|
||||||
const spinner = document.querySelector("#blog_loading_spinner");
|
const spinner = document.querySelector("#blog-loading-spinner");
|
||||||
if (spinner) {
|
if (spinner) {
|
||||||
spinner.classList.add("removed");
|
spinner.classList.add("removed");
|
||||||
setTimeout(() => spinner.remove(), 1100);
|
setTimeout(() => spinner.remove(), 1100);
|
||||||
|
Loading…
Reference in New Issue
Block a user