Compare commits

..

No commits in common. "817019ad1663a6c4b8f09c14487fcee4389a2cc4" and "9e0410f1000b07cc2f8974c9fdc8e8d4e3a3e18e" have entirely different histories.

29 changed files with 65 additions and 286 deletions

View File

@ -1,4 +1,5 @@
using System.Text; using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Web; using OliverBooth.Data.Web;
using OliverBooth.Services; using OliverBooth.Services;

View File

@ -44,7 +44,7 @@ internal sealed class BlogPost : IBlogPost
public DateTimeOffset? Updated { get; internal set; } public DateTimeOffset? Updated { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public Visibility Visibility { get; internal set; } public BlogPostVisibility Visibility { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public int? WordPressId { get; set; } public int? WordPressId { get; set; }

View File

@ -1,15 +1,10 @@
namespace OliverBooth.Data; namespace OliverBooth.Data.Blog;
/// <summary> /// <summary>
/// An enumeration of the possible visibilities of a blog post. /// An enumeration of the possible visibilities of a blog post.
/// </summary> /// </summary>
public enum Visibility public enum BlogPostVisibility
{ {
/// <summary>
/// Used for filtering results. Represents all visibilities.
/// </summary>
None = -1,
/// <summary> /// <summary>
/// The post is private and only visible to the author, or those with the password. /// The post is private and only visible to the author, or those with the password.
/// </summary> /// </summary>

View File

@ -26,7 +26,7 @@ internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
builder.Property(e => e.DisqusDomain).IsRequired(false); builder.Property(e => e.DisqusDomain).IsRequired(false);
builder.Property(e => e.DisqusIdentifier).IsRequired(false); builder.Property(e => e.DisqusIdentifier).IsRequired(false);
builder.Property(e => e.DisqusPath).IsRequired(false); builder.Property(e => e.DisqusPath).IsRequired(false);
builder.Property(e => e.Visibility).HasConversion(new EnumToStringConverter<Visibility>()).IsRequired(); builder.Property(e => e.Visibility).HasConversion(new EnumToStringConverter<BlogPostVisibility>()).IsRequired();
builder.Property(e => e.Password).HasMaxLength(255).IsRequired(false); builder.Property(e => e.Password).HasMaxLength(255).IsRequired(false);
builder.Property(e => e.Tags).IsRequired() builder.Property(e => e.Tags).IsRequired()
.HasConversion( .HasConversion(

View File

@ -85,7 +85,7 @@ public interface IBlogPost
/// Gets the visibility of the post. /// Gets the visibility of the post.
/// </summary> /// </summary>
/// <value>The visibility of the post.</value> /// <value>The visibility of the post.</value>
Visibility Visibility { get; } BlogPostVisibility Visibility { get; }
/// <summary> /// <summary>
/// Gets the WordPress ID of the post. /// Gets the WordPress ID of the post.

View File

@ -1,10 +0,0 @@
namespace OliverBooth.Data.Mastodon;
public enum AttachmentType
{
Unknown,
Image,
GifV,
Video,
Audio
}

View File

@ -1,34 +0,0 @@
using System.Text.Json.Serialization;
namespace OliverBooth.Data.Mastodon;
public sealed class MastodonStatus
{
/// <summary>
/// Gets the content of the status.
/// </summary>
/// <value>The content.</value>
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
/// <summary>
/// Gets the date and time at which this status was posted.
/// </summary>
/// <value>The post timestamp.</value>
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets the media attachments for this status.
/// </summary>
/// <value>The media attachments.</value>
[JsonPropertyName("media_attachments")]
public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; } = ArraySegment<MediaAttachment>.Empty;
/// <summary>
/// Gets the original URI of the status.
/// </summary>
/// <value>The original URI.</value>
[JsonPropertyName("url")]
public Uri OriginalUri { get; set; } = null!;
}

View File

@ -1,22 +0,0 @@
namespace OliverBooth.Data.Mastodon;
public sealed class MediaAttachment
{
/// <summary>
/// Gets the preview URL of the attachment.
/// </summary>
/// <value>The preview URL.</value>
public Uri PreviewUrl { get; set; } = null!;
/// <summary>
/// Gets the type of this attachment.
/// </summary>
/// <value>The attachment type.</value>
public AttachmentType Type { get; set; } = AttachmentType.Unknown;
/// <summary>
/// Gets the URL of the attachment.
/// </summary>
/// <value>The URL.</value>
public Uri Url { get; set; } = null!;
}

View File

@ -2,6 +2,7 @@ using NetBarcode;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using Type = System.Type;
namespace OliverBooth.Data.Web; namespace OliverBooth.Data.Web;

View File

@ -23,6 +23,5 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>(); builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>();
builder.Property(e => e.NextPart); builder.Property(e => e.NextPart);
builder.Property(e => e.PreviousPart); builder.Property(e => e.PreviousPart);
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
} }
} }

View File

@ -16,9 +16,8 @@ internal sealed class TutorialFolderConfiguration : IEntityTypeConfiguration<Tut
builder.Property(e => e.Id).IsRequired(); builder.Property(e => e.Id).IsRequired();
builder.Property(e => e.Parent); builder.Property(e => e.Parent);
builder.Property(e => e.Slug).HasMaxLength(50).IsRequired(); builder.Property(e => e.Slug).IsRequired();
builder.Property(e => e.Title).HasMaxLength(255).IsRequired(); builder.Property(e => e.Title).IsRequired();
builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>(); builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>();
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
} }
} }

View File

@ -1,3 +1,5 @@
using SixLabors.ImageSharp;
namespace OliverBooth.Data.Web; namespace OliverBooth.Data.Web;
/// <summary> /// <summary>

View File

@ -64,10 +64,4 @@ public interface ITutorialArticle
/// </summary> /// </summary>
/// <value>The update timestamp, or <see langword="null" /> if this article has not been updated.</value> /// <value>The update timestamp, or <see langword="null" /> if this article has not been updated.</value>
DateTimeOffset? Updated { get; } DateTimeOffset? Updated { get; }
/// <summary>
/// Gets the visibility of this article.
/// </summary>
/// <value>The visibility of the article.</value>
Visibility Visibility { get; }
} }

View File

@ -34,10 +34,4 @@ public interface ITutorialFolder
/// </summary> /// </summary>
/// <value>The title.</value> /// <value>The title.</value>
string Title { get; } string Title { get; }
}
/// <summary>
/// Gets the visibility of this article.
/// </summary>
/// <value>The visibility of the article.</value>
Visibility Visibility { get; }
}

View File

@ -35,9 +35,6 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
/// <inheritdoc /> /// <inheritdoc />
public DateTimeOffset? Updated { get; private set; } public DateTimeOffset? Updated { get; private set; }
/// <inheritdoc />
public Visibility Visibility { get; private set; }
/// <summary> /// <summary>
/// Returns a value indicating whether two instances of <see cref="TutorialArticle" /> are equal. /// Returns a value indicating whether two instances of <see cref="TutorialArticle" /> are equal.
/// </summary> /// </summary>

View File

@ -20,9 +20,6 @@ internal sealed class TutorialFolder : IEquatable<TutorialFolder>, ITutorialFold
/// <inheritdoc /> /// <inheritdoc />
public string Title { get; private set; } = string.Empty; public string Title { get; private set; } = string.Empty;
/// <inheritdoc />
public Visibility Visibility { get; private set; }
/// <summary> /// <summary>
/// Returns a value indicating whether two instances of <see cref="TutorialFolder" /> are equal. /// Returns a value indicating whether two instances of <see cref="TutorialFolder" /> are equal.
/// </summary> /// </summary>
@ -83,4 +80,4 @@ internal sealed class TutorialFolder : IEquatable<TutorialFolder>, ITutorialFold
// ReSharper disable once NonReadonlyMemberInGetHashCode // ReSharper disable once NonReadonlyMemberInGetHashCode
return Id; return Id;
} }
} }

View File

@ -1,6 +1,5 @@
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}" @page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
@using Humanizer @using Humanizer
@using OliverBooth.Data
@using OliverBooth.Data.Blog @using OliverBooth.Data.Blog
@using OliverBooth.Services @using OliverBooth.Services
@inject IBlogPostService BlogPostService @inject IBlogPostService BlogPostService
@ -45,13 +44,13 @@
@switch (post.Visibility) @switch (post.Visibility)
{ {
case Visibility.Private: case BlogPostVisibility.Private:
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
This post is private and can only be viewed by those with the password. This post is private and can only be viewed by those with the password.
</div> </div>
break; break;
case Visibility.Unlisted: case BlogPostVisibility.Unlisted:
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
This post is unlisted and can only be viewed by those with the link. This post is unlisted and can only be viewed by those with the link.
</div> </div>

View File

@ -1,44 +1,10 @@
@page @page
@using Humanizer
@using OliverBooth.Data.Mastodon
@using OliverBooth.Services
@model Index @model Index
@inject IMastodonService MastodonService
@{ @{
ViewData["Title"] = "Blog"; ViewData["Title"] = "Blog";
MastodonStatus latestStatus = MastodonService.GetLatestStatus();
} }
<div class="card text-center mastodon-update-card">
<div class="card-body">
@Html.Raw(latestStatus.Content)
@foreach (MediaAttachment attachment in latestStatus.MediaAttachments)
{
switch (attachment.Type)
{
case AttachmentType.Audio:
<p><audio controls="controls" src="@attachment.Url"></audio></p>
break;
case AttachmentType.Video:
<p><video controls="controls" class="figure-img img-fluid" src="@attachment.Url"></video></p>
break;
case AttachmentType.Image:
case AttachmentType.GifV:
<p><img class="figure-img img-fluid" src="@attachment.Url"></p>
break;
}
}
</div>
<div class="card-footer text-muted">
<abbr title="@latestStatus.CreatedAt.ToString("F")">@latestStatus.CreatedAt.Humanize()</abbr>
&bull;
<a href="@latestStatus.OriginalUri" target="_blank">View on Mastodon</a>
</div>
</div>
<div id="all-blog-posts"> <div id="all-blog-posts">
@await Html.PartialAsync("_LoadingSpinner") @await Html.PartialAsync("_LoadingSpinner")
</div> </div>

View File

@ -94,17 +94,9 @@
<div style="margin:50px 0;"></div> <div style="margin:50px 0;"></div>
@if (DateTimeOffset.UtcNow < new DateTime(2024, 03, 08)) <div id="usa-countdown" class="container">
{ <p>00 : 00 : 00 : 00</p>
<div id="usa-countdown" class="container"> </div>
<div class="row">
<div class="col-3 usa-countdown-element" id="usa-countdown-days">00</div>
<div class="col-3 usa-countdown-element" id="usa-countdown-hours">00</div>
<div class="col-3 usa-countdown-element" id="usa-countdown-minutes">00</div>
<div class="col-3 usa-countdown-element" id="usa-countdown-seconds">00</div>
</div>
</div>
}
<div style="margin:50px 0;"></div> <div style="margin:50px 0;"></div>

View File

@ -32,13 +32,11 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
builder.Services.AddDbContextFactory<BlogContext>(); builder.Services.AddDbContextFactory<BlogContext>();
builder.Services.AddDbContextFactory<WebContext>(); builder.Services.AddDbContextFactory<WebContext>();
builder.Services.AddHttpClient();
builder.Services.AddSingleton<IContactService, ContactService>(); builder.Services.AddSingleton<IContactService, ContactService>();
builder.Services.AddSingleton<ITemplateService, TemplateService>(); builder.Services.AddSingleton<ITemplateService, TemplateService>();
builder.Services.AddSingleton<IBlogPostService, BlogPostService>(); builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
builder.Services.AddSingleton<IBlogUserService, BlogUserService>(); builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
builder.Services.AddSingleton<IProjectService, ProjectService>(); builder.Services.AddSingleton<IProjectService, ProjectService>();
builder.Services.AddSingleton<IMastodonService, MastodonService>();
builder.Services.AddSingleton<ITutorialService, TutorialService>(); builder.Services.AddSingleton<ITutorialService, TutorialService>();
builder.Services.AddSingleton<IReadingListService, ReadingListService>(); builder.Services.AddSingleton<IReadingListService, ReadingListService>();
builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation();

View File

@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using Humanizer; using Humanizer;
using Markdig; using Markdig;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OliverBooth.Data;
using OliverBooth.Data.Blog; using OliverBooth.Data.Blog;
namespace OliverBooth.Services; namespace OliverBooth.Services;
@ -45,7 +44,7 @@ internal sealed class BlogPostService : IBlogPostService
{ {
using BlogContext context = _dbContextFactory.CreateDbContext(); using BlogContext context = _dbContextFactory.CreateDbContext();
IQueryable<BlogPost> ordered = context.BlogPosts IQueryable<BlogPost> ordered = context.BlogPosts
.Where(p => p.Visibility == Visibility.Published) .Where(p => p.Visibility == BlogPostVisibility.Published)
.OrderByDescending(post => post.Published); .OrderByDescending(post => post.Published);
if (limit > -1) if (limit > -1)
{ {
@ -60,7 +59,7 @@ internal sealed class BlogPostService : IBlogPostService
{ {
using BlogContext context = _dbContextFactory.CreateDbContext(); using BlogContext context = _dbContextFactory.CreateDbContext();
return context.BlogPosts return context.BlogPosts
.Where(p => p.Visibility == Visibility.Published) .Where(p => p.Visibility == BlogPostVisibility.Published)
.OrderByDescending(post => post.Published) .OrderByDescending(post => post.Published)
.Skip(page * pageSize) .Skip(page * pageSize)
.Take(pageSize) .Take(pageSize)
@ -72,7 +71,7 @@ internal sealed class BlogPostService : IBlogPostService
{ {
using BlogContext context = _dbContextFactory.CreateDbContext(); using BlogContext context = _dbContextFactory.CreateDbContext();
return context.BlogPosts return context.BlogPosts
.Where(p => p.Visibility == Visibility.Published) .Where(p => p.Visibility == BlogPostVisibility.Published)
.OrderBy(post => post.Published) .OrderBy(post => post.Published)
.FirstOrDefault(post => post.Published > blogPost.Published); .FirstOrDefault(post => post.Published > blogPost.Published);
} }
@ -82,7 +81,7 @@ internal sealed class BlogPostService : IBlogPostService
{ {
using BlogContext context = _dbContextFactory.CreateDbContext(); using BlogContext context = _dbContextFactory.CreateDbContext();
return context.BlogPosts return context.BlogPosts
.Where(p => p.Visibility == Visibility.Published) .Where(p => p.Visibility == BlogPostVisibility.Published)
.OrderByDescending(post => post.Published) .OrderByDescending(post => post.Published)
.FirstOrDefault(post => post.Published < blogPost.Published); .FirstOrDefault(post => post.Published < blogPost.Published);
} }

View File

@ -1,12 +0,0 @@
using OliverBooth.Data.Mastodon;
namespace OliverBooth.Services;
public interface IMastodonService
{
/// <summary>
/// Gets the latest status posted to Mastodon.
/// </summary>
/// <returns>The latest status.</returns>
MastodonStatus GetLatestStatus();
}

View File

@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data;
using OliverBooth.Data.Web; using OliverBooth.Data.Web;
namespace OliverBooth.Services; namespace OliverBooth.Services;
@ -13,17 +12,14 @@ public interface ITutorialService
/// Gets the articles within a tutorial folder. /// Gets the articles within a tutorial folder.
/// </summary> /// </summary>
/// <param name="folder">The folder whose articles to retrieve.</param> /// <param name="folder">The folder whose articles to retrieve.</param>
/// <param name="visibility">The visibility to filter by. -1 does not filter.</param>
/// <returns>A read-only view of the articles in the folder.</returns> /// <returns>A read-only view of the articles in the folder.</returns>
IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder, Visibility visibility = Visibility.None); IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder);
/// <summary> /// <summary>
/// Gets the tutorial folders within a specified folder. /// Gets the tutorial folders within a specified folder.
/// </summary> /// </summary>
/// <param name="parent">The parent folder.</param>
/// <param name="visibility">The visibility to filter by. -1 does not filter.</param>
/// <returns>A read-only view of the subfolders in the folder.</returns> /// <returns>A read-only view of the subfolders in the folder.</returns>
IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null, Visibility visibility = Visibility.None); IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null);
/// <summary> /// <summary>
/// Gets a folder by its ID. /// Gets a folder by its ID.

View File

@ -1,35 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using OliverBooth.Data.Mastodon;
namespace OliverBooth.Services;
internal sealed class MastodonService : IMastodonService
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
Converters = { new JsonStringEnumConverter() },
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
private readonly HttpClient _httpClient;
public MastodonService(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <inheritdoc />
public MastodonStatus GetLatestStatus()
{
string token = Environment.GetEnvironmentVariable("MASTODON_TOKEN") ?? string.Empty;
string account = Environment.GetEnvironmentVariable("MASTODON_ACCOUNT") ?? string.Empty;
using var request = new HttpRequestMessage();
request.Headers.Add("Authorization", $"Bearer {token}");
request.RequestUri = new Uri($"https://mastodon.olivr.me/api/v1/accounts/{account}/statuses");
using HttpResponseMessage response = _httpClient.Send(request);
using var stream = response.Content.ReadAsStream();
var statuses = JsonSerializer.Deserialize<MastodonStatus[]>(stream, JsonSerializerOptions);
return statuses?[0]!;
}
}

View File

@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using Cysharp.Text; using Cysharp.Text;
using Markdig; using Markdig;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OliverBooth.Data;
using OliverBooth.Data.Web; using OliverBooth.Data.Web;
namespace OliverBooth.Services; namespace OliverBooth.Services;
@ -24,28 +23,20 @@ internal sealed class TutorialService : ITutorialService
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder, public IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder)
Visibility visibility = Visibility.None)
{ {
if (folder is null) throw new ArgumentNullException(nameof(folder)); if (folder is null) throw new ArgumentNullException(nameof(folder));
using WebContext context = _dbContextFactory.CreateDbContext(); using WebContext context = _dbContextFactory.CreateDbContext();
IQueryable<TutorialArticle> articles = context.TutorialArticles.Where(a => a.Folder == folder.Id); return context.TutorialArticles.Where(a => a.Folder == folder.Id).ToArray();
if (visibility != Visibility.None) articles = articles.Where(a => a.Visibility == visibility);
return articles.ToArray();
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null, public IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null)
Visibility visibility = Visibility.None)
{ {
using WebContext context = _dbContextFactory.CreateDbContext(); using WebContext context = _dbContextFactory.CreateDbContext();
IQueryable<TutorialFolder> folders = context.TutorialFolders; if (parent is null) return context.TutorialFolders.Where(f => f.Parent == null).ToArray();
return context.TutorialFolders.Where(a => a.Parent == parent.Id).ToArray();
folders = parent is null ? folders.Where(f => f.Parent == null) : folders.Where(f => f.Parent == parent.Id);
if (visibility != Visibility.None) folders = folders.Where(a => a.Visibility == visibility);
return folders.ToArray();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -19,5 +19,3 @@ services:
environment: environment:
- SSL_CERT_PATH=${SSL_CERT_PATH} - SSL_CERT_PATH=${SSL_CERT_PATH}
- SSL_KEY_PATH=${SSL_KEY_PATH} - SSL_KEY_PATH=${SSL_KEY_PATH}
- MASTODON_TOKEN=${MASTODON_TOKEN}
- MASTODON_ACCOUNT=${MASTODON_ACCOUNT}

View File

@ -365,27 +365,25 @@ td.trim-p p:last-child {
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
border-radius: 10px;
cursor: pointer; p {
* {
cursor: pointer;
}
.usa-countdown-element {
margin: 10px 0;
padding: 5px;
font-family: "Gabarito", sans-serif; font-family: "Gabarito", sans-serif;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
font-size: 3em; font-size: 3em;
border-right: 2px solid #fff; margin: 0;
border-left: 2px solid #fff; padding: 0;
&:first-child { a {
border-left: none; transition: color 250ms;
} }
&:last-child {
border-right: none; a:link, a:visited, a:active {
color: #fff;
}
a:hover {
color: #03A9F4;
} }
} }
} }
@ -402,17 +400,4 @@ td.trim-p p:last-child {
color: #03A9F4; color: #03A9F4;
background-color: #1E1E1E !important; background-color: #1E1E1E !important;
} }
}
.mastodon-update-card.card {
background-color: desaturate(darken(#6364FF, 50%), 50%);
margin-bottom: 50px;
p:last-child {
margin-bottom: 0;
}
button.btn.btn-mastodon {
background-color: #6364FF;
}
} }

View File

@ -82,33 +82,6 @@ class UI {
UI.updateProjectCards(element); UI.updateProjectCards(element);
} }
public static updateUsaCountdown(element?: Element){
element = element || document.getElementById("usa-countdown");
const daysElement = element.querySelector("#usa-countdown-days");
const hoursElement = element.querySelector("#usa-countdown-hours");
const minutesElement = element.querySelector("#usa-countdown-minutes");
const secondsElement = element.querySelector("#usa-countdown-seconds");
const start = new Date().getTime();
const end = Date.UTC(2024, 2, 7, 13, 20);
const diff = end - start;
let days = Math.floor(diff / (1000 * 60 * 60 * 24));
let hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
let minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
let seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (days < 0) days = 0
if (hours < 0) hours = 0;
if (minutes < 0) minutes = 0;
if (seconds < 0) seconds = 0;
daysElement.innerHTML = days.toString().padStart(2, '0');
hoursElement.innerHTML = hours.toString().padStart(2, '0');
minutesElement.innerHTML = minutes.toString().padStart(2, '0');
secondsElement.innerHTML = seconds.toString().padStart(2, '0');
}
/** /**
* Adds Bootstrap tooltips to all elements with a title attribute. * Adds Bootstrap tooltips to all elements with a title attribute.
* @param element The element to search for elements with a title attribute in. * @param element The element to search for elements with a title attribute in.

View File

@ -97,11 +97,27 @@ declare const Prism: any;
} }
UI.updateUI(); UI.updateUI();
setInterval(() => {
const countdown = document.querySelector("#usa-countdown p");
const start = new Date().getTime();
const end = Date.UTC(2024, 2, 7, 13, 20);
const diff = end - start;
let days = Math.floor(diff / (1000 * 60 * 60 * 24));
let hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
let minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
let seconds = Math.floor((diff % (1000 * 60)) / 1000);
const usaCountdown = document.getElementById("usa-countdown"); if (days < 0) days = 0
if (usaCountdown) { if (hours < 0) hours = 0;
usaCountdown.addEventListener("click", () => window.location.href = "/blog/2024/02/19/the-american"); if (minutes < 0) minutes = 0;
UI.updateUsaCountdown(usaCountdown); if (seconds < 0) seconds = 0;
setInterval(() => UI.updateUsaCountdown(usaCountdown), 1000);
} const blogUrl = '/blog/2024/02/19/the-american';
const dayStr = days.toString().padStart(2, '0');
const hourStr = hours.toString().padStart(2, '0');
const minuteStr = minutes.toString().padStart(2, '0');
const secondStr = seconds.toString().padStart(2, '0');
countdown.innerHTML = `<a href="${blogUrl}">${dayStr} : ${hourStr} : ${minuteStr} : ${secondStr}</a>`;
}, 1000);
})(); })();