Compare commits
10 Commits
9e0410f100
...
817019ad16
Author | SHA1 | Date | |
---|---|---|---|
817019ad16 | |||
bd1e9dac1f | |||
9074cf5210 | |||
577f3b0148 | |||
8629f8f963 | |||
0aa9754714 | |||
70f167c9c3 | |||
21be5e9622 | |||
279d824772 | |||
c7cd016baf |
@ -1,5 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Services;
|
||||
|
@ -44,7 +44,7 @@ internal sealed class BlogPost : IBlogPost
|
||||
public DateTimeOffset? Updated { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BlogPostVisibility Visibility { get; internal set; }
|
||||
public Visibility Visibility { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? WordPressId { get; set; }
|
||||
|
@ -26,7 +26,7 @@ internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
||||
builder.Property(e => e.DisqusDomain).IsRequired(false);
|
||||
builder.Property(e => e.DisqusIdentifier).IsRequired(false);
|
||||
builder.Property(e => e.DisqusPath).IsRequired(false);
|
||||
builder.Property(e => e.Visibility).HasConversion(new EnumToStringConverter<BlogPostVisibility>()).IsRequired();
|
||||
builder.Property(e => e.Visibility).HasConversion(new EnumToStringConverter<Visibility>()).IsRequired();
|
||||
builder.Property(e => e.Password).HasMaxLength(255).IsRequired(false);
|
||||
builder.Property(e => e.Tags).IsRequired()
|
||||
.HasConversion(
|
||||
|
@ -85,7 +85,7 @@ public interface IBlogPost
|
||||
/// Gets the visibility of the post.
|
||||
/// </summary>
|
||||
/// <value>The visibility of the post.</value>
|
||||
BlogPostVisibility Visibility { get; }
|
||||
Visibility Visibility { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the WordPress ID of the post.
|
||||
|
10
OliverBooth/Data/Mastodon/AttachmentType.cs
Normal file
10
OliverBooth/Data/Mastodon/AttachmentType.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace OliverBooth.Data.Mastodon;
|
||||
|
||||
public enum AttachmentType
|
||||
{
|
||||
Unknown,
|
||||
Image,
|
||||
GifV,
|
||||
Video,
|
||||
Audio
|
||||
}
|
34
OliverBooth/Data/Mastodon/MastodonStatus.cs
Normal file
34
OliverBooth/Data/Mastodon/MastodonStatus.cs
Normal file
@ -0,0 +1,34 @@
|
||||
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!;
|
||||
}
|
22
OliverBooth/Data/Mastodon/MediaAttachment.cs
Normal file
22
OliverBooth/Data/Mastodon/MediaAttachment.cs
Normal file
@ -0,0 +1,22 @@
|
||||
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!;
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
namespace OliverBooth.Data.Blog;
|
||||
namespace OliverBooth.Data;
|
||||
|
||||
/// <summary>
|
||||
/// An enumeration of the possible visibilities of a blog post.
|
||||
/// </summary>
|
||||
public enum BlogPostVisibility
|
||||
public enum Visibility
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for filtering results. Represents all visibilities.
|
||||
/// </summary>
|
||||
None = -1,
|
||||
|
||||
/// <summary>
|
||||
/// The post is private and only visible to the author, or those with the password.
|
||||
/// </summary>
|
@ -2,7 +2,6 @@ using NetBarcode;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
|
@ -23,5 +23,6 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
|
||||
builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>();
|
||||
builder.Property(e => e.NextPart);
|
||||
builder.Property(e => e.PreviousPart);
|
||||
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ internal sealed class TutorialFolderConfiguration : IEntityTypeConfiguration<Tut
|
||||
|
||||
builder.Property(e => e.Id).IsRequired();
|
||||
builder.Property(e => e.Parent);
|
||||
builder.Property(e => e.Slug).IsRequired();
|
||||
builder.Property(e => e.Title).IsRequired();
|
||||
builder.Property(e => e.Slug).HasMaxLength(50).IsRequired();
|
||||
builder.Property(e => e.Title).HasMaxLength(255).IsRequired();
|
||||
builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>();
|
||||
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <summary>
|
||||
|
@ -64,4 +64,10 @@ public interface ITutorialArticle
|
||||
/// </summary>
|
||||
/// <value>The update timestamp, or <see langword="null" /> if this article has not been updated.</value>
|
||||
DateTimeOffset? Updated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visibility of this article.
|
||||
/// </summary>
|
||||
/// <value>The visibility of the article.</value>
|
||||
Visibility Visibility { get; }
|
||||
}
|
||||
|
@ -34,4 +34,10 @@ public interface ITutorialFolder
|
||||
/// </summary>
|
||||
/// <value>The title.</value>
|
||||
string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visibility of this article.
|
||||
/// </summary>
|
||||
/// <value>The visibility of the article.</value>
|
||||
Visibility Visibility { get; }
|
||||
}
|
@ -35,6 +35,9 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset? Updated { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Visibility Visibility { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="TutorialArticle" /> are equal.
|
||||
/// </summary>
|
||||
|
@ -20,6 +20,9 @@ internal sealed class TutorialFolder : IEquatable<TutorialFolder>, ITutorialFold
|
||||
/// <inheritdoc />
|
||||
public string Title { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Visibility Visibility { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="TutorialFolder" /> are equal.
|
||||
/// </summary>
|
||||
|
@ -1,5 +1,6 @@
|
||||
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
|
||||
@using Humanizer
|
||||
@using OliverBooth.Data
|
||||
@using OliverBooth.Data.Blog
|
||||
@using OliverBooth.Services
|
||||
@inject IBlogPostService BlogPostService
|
||||
@ -44,13 +45,13 @@
|
||||
|
||||
@switch (post.Visibility)
|
||||
{
|
||||
case BlogPostVisibility.Private:
|
||||
case Visibility.Private:
|
||||
<div class="alert alert-danger" role="alert">
|
||||
This post is private and can only be viewed by those with the password.
|
||||
</div>
|
||||
break;
|
||||
|
||||
case BlogPostVisibility.Unlisted:
|
||||
case Visibility.Unlisted:
|
||||
<div class="alert alert-warning" role="alert">
|
||||
This post is unlisted and can only be viewed by those with the link.
|
||||
</div>
|
||||
|
@ -1,10 +1,44 @@
|
||||
@page
|
||||
@using Humanizer
|
||||
@using OliverBooth.Data.Mastodon
|
||||
@using OliverBooth.Services
|
||||
@model Index
|
||||
@inject IMastodonService MastodonService
|
||||
|
||||
@{
|
||||
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>
|
||||
•
|
||||
<a href="@latestStatus.OriginalUri" target="_blank">View on Mastodon</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="all-blog-posts">
|
||||
@await Html.PartialAsync("_LoadingSpinner")
|
||||
</div>
|
||||
|
@ -94,9 +94,17 @@
|
||||
|
||||
<div style="margin:50px 0;"></div>
|
||||
|
||||
<div id="usa-countdown" class="container">
|
||||
<p>00 : 00 : 00 : 00</p>
|
||||
</div>
|
||||
@if (DateTimeOffset.UtcNow < new DateTime(2024, 03, 08))
|
||||
{
|
||||
<div id="usa-countdown" class="container">
|
||||
<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>
|
||||
|
||||
|
@ -32,11 +32,13 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||
|
||||
builder.Services.AddDbContextFactory<BlogContext>();
|
||||
builder.Services.AddDbContextFactory<WebContext>();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddSingleton<IContactService, ContactService>();
|
||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
||||
builder.Services.AddSingleton<IProjectService, ProjectService>();
|
||||
builder.Services.AddSingleton<IMastodonService, MastodonService>();
|
||||
builder.Services.AddSingleton<ITutorialService, TutorialService>();
|
||||
builder.Services.AddSingleton<IReadingListService, ReadingListService>();
|
||||
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
||||
|
@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Humanizer;
|
||||
using Markdig;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data;
|
||||
using OliverBooth.Data.Blog;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
@ -44,7 +45,7 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
{
|
||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||
IQueryable<BlogPost> ordered = context.BlogPosts
|
||||
.Where(p => p.Visibility == BlogPostVisibility.Published)
|
||||
.Where(p => p.Visibility == Visibility.Published)
|
||||
.OrderByDescending(post => post.Published);
|
||||
if (limit > -1)
|
||||
{
|
||||
@ -59,7 +60,7 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
{
|
||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.BlogPosts
|
||||
.Where(p => p.Visibility == BlogPostVisibility.Published)
|
||||
.Where(p => p.Visibility == Visibility.Published)
|
||||
.OrderByDescending(post => post.Published)
|
||||
.Skip(page * pageSize)
|
||||
.Take(pageSize)
|
||||
@ -71,7 +72,7 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
{
|
||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.BlogPosts
|
||||
.Where(p => p.Visibility == BlogPostVisibility.Published)
|
||||
.Where(p => p.Visibility == Visibility.Published)
|
||||
.OrderBy(post => post.Published)
|
||||
.FirstOrDefault(post => post.Published > blogPost.Published);
|
||||
}
|
||||
@ -81,7 +82,7 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
{
|
||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.BlogPosts
|
||||
.Where(p => p.Visibility == BlogPostVisibility.Published)
|
||||
.Where(p => p.Visibility == Visibility.Published)
|
||||
.OrderByDescending(post => post.Published)
|
||||
.FirstOrDefault(post => post.Published < blogPost.Published);
|
||||
}
|
||||
|
12
OliverBooth/Services/IMastodonService.cs
Normal file
12
OliverBooth/Services/IMastodonService.cs
Normal file
@ -0,0 +1,12 @@
|
||||
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();
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using OliverBooth.Data;
|
||||
using OliverBooth.Data.Web;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
@ -12,14 +13,17 @@ public interface ITutorialService
|
||||
/// Gets the articles within a tutorial folder.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder);
|
||||
IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder, Visibility visibility = Visibility.None);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tutorial folders within a specified folder.
|
||||
/// </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>
|
||||
IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null);
|
||||
IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null, Visibility visibility = Visibility.None);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a folder by its ID.
|
||||
|
35
OliverBooth/Services/MastodonService.cs
Normal file
35
OliverBooth/Services/MastodonService.cs
Normal file
@ -0,0 +1,35 @@
|
||||
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]!;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Cysharp.Text;
|
||||
using Markdig;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data;
|
||||
using OliverBooth.Data.Web;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
@ -23,20 +24,28 @@ internal sealed class TutorialService : ITutorialService
|
||||
}
|
||||
|
||||
/// <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));
|
||||
|
||||
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.TutorialArticles.Where(a => a.Folder == folder.Id).ToArray();
|
||||
IQueryable<TutorialArticle> articles = context.TutorialArticles.Where(a => a.Folder == folder.Id);
|
||||
|
||||
if (visibility != Visibility.None) articles = articles.Where(a => a.Visibility == visibility);
|
||||
return articles.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null)
|
||||
public IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null,
|
||||
Visibility visibility = Visibility.None)
|
||||
{
|
||||
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||
if (parent is null) return context.TutorialFolders.Where(f => f.Parent == null).ToArray();
|
||||
return context.TutorialFolders.Where(a => a.Parent == parent.Id).ToArray();
|
||||
IQueryable<TutorialFolder> folders = context.TutorialFolders;
|
||||
|
||||
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 />
|
||||
|
@ -19,3 +19,5 @@ services:
|
||||
environment:
|
||||
- SSL_CERT_PATH=${SSL_CERT_PATH}
|
||||
- SSL_KEY_PATH=${SSL_KEY_PATH}
|
||||
- MASTODON_TOKEN=${MASTODON_TOKEN}
|
||||
- MASTODON_ACCOUNT=${MASTODON_ACCOUNT}
|
||||
|
@ -365,25 +365,27 @@ td.trim-p p:last-child {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
* {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
.usa-countdown-element {
|
||||
margin: 10px 0;
|
||||
padding: 5px;
|
||||
font-family: "Gabarito", sans-serif;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
font-size: 3em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-right: 2px solid #fff;
|
||||
border-left: 2px solid #fff;
|
||||
|
||||
a {
|
||||
transition: color 250ms;
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
a:link, a:visited, a:active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #03A9F4;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -401,3 +403,16 @@ td.trim-p p:last-child {
|
||||
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;
|
||||
}
|
||||
}
|
27
src/ts/UI.ts
27
src/ts/UI.ts
@ -82,6 +82,33 @@ class UI {
|
||||
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.
|
||||
* @param element The element to search for elements with a title attribute in.
|
||||
|
@ -98,26 +98,10 @@ declare const Prism: any;
|
||||
|
||||
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);
|
||||
|
||||
if (days < 0) days = 0
|
||||
if (hours < 0) hours = 0;
|
||||
if (minutes < 0) minutes = 0;
|
||||
if (seconds < 0) seconds = 0;
|
||||
|
||||
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);
|
||||
const usaCountdown = document.getElementById("usa-countdown");
|
||||
if (usaCountdown) {
|
||||
usaCountdown.addEventListener("click", () => window.location.href = "/blog/2024/02/19/the-american");
|
||||
UI.updateUsaCountdown(usaCountdown);
|
||||
setInterval(() => UI.updateUsaCountdown(usaCountdown), 1000);
|
||||
}
|
||||
})();
|
||||
|
Loading…
Reference in New Issue
Block a user