diff --git a/OliverBooth/Controllers/FormattedBlacklist.cs b/OliverBooth/Controllers/FormattedBlacklist.cs index 9f29076..bce6143 100644 --- a/OliverBooth/Controllers/FormattedBlacklist.cs +++ b/OliverBooth/Controllers/FormattedBlacklist.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Text.Json; using Microsoft.AspNetCore.Mvc; using OliverBooth.Data.Web; using OliverBooth.Services; diff --git a/OliverBooth/Data/Mastodon/AttachmentType.cs b/OliverBooth/Data/Mastodon/AttachmentType.cs new file mode 100644 index 0000000..6d0b5e1 --- /dev/null +++ b/OliverBooth/Data/Mastodon/AttachmentType.cs @@ -0,0 +1,10 @@ +namespace OliverBooth.Data.Mastodon; + +public enum AttachmentType +{ + Unknown, + Image, + GifV, + Video, + Audio +} diff --git a/OliverBooth/Data/Mastodon/MastodonStatus.cs b/OliverBooth/Data/Mastodon/MastodonStatus.cs new file mode 100644 index 0000000..a4d3ebf --- /dev/null +++ b/OliverBooth/Data/Mastodon/MastodonStatus.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace OliverBooth.Data.Mastodon; + +public sealed class MastodonStatus +{ + /// + /// Gets the content of the status. + /// + /// The content. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// + /// Gets the date and time at which this status was posted. + /// + /// The post timestamp. + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + /// + /// Gets the media attachments for this status. + /// + /// The media attachments. + [JsonPropertyName("media_attachments")] + public IReadOnlyList MediaAttachments { get; set; } = ArraySegment.Empty; + + /// + /// Gets the original URI of the status. + /// + /// The original URI. + [JsonPropertyName("url")] + public Uri OriginalUri { get; set; } = null!; +} diff --git a/OliverBooth/Data/Mastodon/MediaAttachment.cs b/OliverBooth/Data/Mastodon/MediaAttachment.cs new file mode 100644 index 0000000..ddc3307 --- /dev/null +++ b/OliverBooth/Data/Mastodon/MediaAttachment.cs @@ -0,0 +1,22 @@ +namespace OliverBooth.Data.Mastodon; + +public sealed class MediaAttachment +{ + /// + /// Gets the preview URL of the attachment. + /// + /// The preview URL. + public Uri PreviewUrl { get; set; } = null!; + + /// + /// Gets the type of this attachment. + /// + /// The attachment type. + public AttachmentType Type { get; set; } = AttachmentType.Unknown; + + /// + /// Gets the URL of the attachment. + /// + /// The URL. + public Uri Url { get; set; } = null!; +} \ No newline at end of file diff --git a/OliverBooth/Data/Web/Book.cs b/OliverBooth/Data/Web/Book.cs index 465e0f3..278aa4e 100644 --- a/OliverBooth/Data/Web/Book.cs +++ b/OliverBooth/Data/Web/Book.cs @@ -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; diff --git a/OliverBooth/Data/Web/IBook.cs b/OliverBooth/Data/Web/IBook.cs index 05c7f32..5c71d85 100644 --- a/OliverBooth/Data/Web/IBook.cs +++ b/OliverBooth/Data/Web/IBook.cs @@ -1,5 +1,3 @@ -using SixLabors.ImageSharp; - namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj index 1c6ddab..6edb58b 100644 --- a/OliverBooth/OliverBooth.csproj +++ b/OliverBooth/OliverBooth.csproj @@ -11,6 +11,7 @@ + diff --git a/OliverBooth/Pages/Blog/Index.cshtml b/OliverBooth/Pages/Blog/Index.cshtml index d558cdf..1109038 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml +++ b/OliverBooth/Pages/Blog/Index.cshtml @@ -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(); } +
+
+ @Html.Raw(latestStatus.Content) + @foreach (MediaAttachment attachment in latestStatus.MediaAttachments) + { + switch (attachment.Type) + { + case AttachmentType.Audio: +

+ break; + + case AttachmentType.Video: +

+ break; + + case AttachmentType.Image: + case AttachmentType.GifV: +

+ break; + } + } +
+ +
+
@await Html.PartialAsync("_LoadingSpinner")
diff --git a/OliverBooth/Pages/Shared/_Layout.cshtml b/OliverBooth/Pages/Shared/_Layout.cshtml index a2908c3..b1d9a88 100644 --- a/OliverBooth/Pages/Shared/_Layout.cshtml +++ b/OliverBooth/Pages/Shared/_Layout.cshtml @@ -94,9 +94,17 @@
-
-

00 : 00 : 00 : 00

-
+@if (DateTimeOffset.UtcNow < new DateTime(2024, 03, 08)) +{ +
+
+
00
+
00
+
00
+
00
+
+
+}
diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index dc6ba32..99c7701 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -32,12 +32,14 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder() builder.Services.AddDbContextFactory(); builder.Services.AddDbContextFactory(); +builder.Services.AddHttpClient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); builder.Services.AddControllersWithViews(); diff --git a/OliverBooth/Services/IMastodonService.cs b/OliverBooth/Services/IMastodonService.cs new file mode 100644 index 0000000..2126d46 --- /dev/null +++ b/OliverBooth/Services/IMastodonService.cs @@ -0,0 +1,12 @@ +using OliverBooth.Data.Mastodon; + +namespace OliverBooth.Services; + +public interface IMastodonService +{ + /// + /// Gets the latest status posted to Mastodon. + /// + /// The latest status. + MastodonStatus GetLatestStatus(); +} \ No newline at end of file diff --git a/OliverBooth/Services/MastodonService.cs b/OliverBooth/Services/MastodonService.cs new file mode 100644 index 0000000..fdc7486 --- /dev/null +++ b/OliverBooth/Services/MastodonService.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using HtmlAgilityPack; +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; + } + + /// + 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(stream, JsonSerializerOptions); + + MastodonStatus status = statuses?[0]!; + var document = new HtmlDocument(); + document.LoadHtml(status.Content); + + HtmlNodeCollection links = document.DocumentNode.SelectNodes("//a"); + foreach (HtmlNode link in links) link.InnerHtml = link.InnerText; + + status.Content = document.DocumentNode.OuterHtml; + return status; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 9668736..3a9ace9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/src/scss/app.scss b/src/scss/app.scss index 34872a3..19e3129 100644 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -365,25 +365,27 @@ td.trim-p p:last-child { background-position: center; background-repeat: no-repeat; background-size: cover; - - p { + border-radius: 10px; + cursor: pointer; + * { + cursor: pointer; + } + + .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; - - a { - transition: color 250ms; - } + border-right: 2px solid #fff; + border-left: 2px solid #fff; - a:link, a:visited, a:active { - color: #fff; + &:first-child { + border-left: none; } - - a:hover { - color: #03A9F4; + &:last-child { + border-right: none; } } } @@ -400,4 +402,17 @@ td.trim-p p:last-child { color: #03A9F4; 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; + } } \ No newline at end of file diff --git a/src/ts/UI.ts b/src/ts/UI.ts index 969fde3..34310f8 100644 --- a/src/ts/UI.ts +++ b/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. diff --git a/src/ts/app.ts b/src/ts/app.ts index 75782ac..9d0723e 100644 --- a/src/ts/app.ts +++ b/src/ts/app.ts @@ -97,27 +97,11 @@ 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 = `${dayStr} : ${hourStr} : ${minuteStr} : ${secondStr}`; - }, 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); + } })();