Improvements to a11y and scripts
This commit is contained in:
parent
a335aff2d3
commit
d022fab5d6
17 changed files with 384 additions and 214 deletions
|
@ -1,3 +1,3 @@
|
||||||
src/components/DarkModeScript.astro
|
src/components/AutoDarkMode.astro
|
||||||
.astro/
|
.astro/
|
||||||
dist/
|
dist/
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"version": "1.7.5",
|
"version": "1.7.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"version": "1.7.5",
|
"version": "1.7.6",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.2",
|
"@astrojs/check": "^0.9.2",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.7.5",
|
"version": "1.7.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "astro sync",
|
"postinstall": "astro sync",
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
|
|
|
@ -53,30 +53,34 @@
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem("ageVerified") !== "true") {
|
if (localStorage.getItem("ageVerified") !== "true") {
|
||||||
document.body.appendChild(
|
const fragment = document
|
||||||
document
|
.querySelector<HTMLElementTagNameMap["template"]>("template#template-modal-age-restricted")!
|
||||||
.querySelector<HTMLElementTagNameMap["template"]>("template#template-modal-age-restricted")!
|
.content.cloneNode(true) as DocumentFragment;
|
||||||
.content.cloneNode(true),
|
const modal = fragment.firstElementChild as HTMLElementTagNameMap["div"];
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
location.href = "about:blank";
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
controller.abort();
|
||||||
|
localStorage.setItem("ageVerified", "true");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
modal.remove();
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
);
|
);
|
||||||
const modal = document.querySelector<HTMLElementTagNameMap["div"]>("body > div#modal-age-restricted")!;
|
|
||||||
const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
|
|
||||||
const acceptButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!;
|
|
||||||
function onRejectButtonClick(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
location.href = "about:blank";
|
|
||||||
}
|
|
||||||
function onAcceptButtonClick(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
rejectButton.removeEventListener("click", onRejectButtonClick);
|
|
||||||
acceptButton.removeEventListener("click", onAcceptButtonClick);
|
|
||||||
localStorage.setItem("ageVerified", "true");
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
modal.remove();
|
|
||||||
}
|
|
||||||
rejectButton.addEventListener("click", onRejectButtonClick);
|
|
||||||
acceptButton.addEventListener("click", onAcceptButtonClick);
|
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
rejectButton.focus();
|
document.body.appendChild(fragment);
|
||||||
|
document.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")?.focus();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
4
src/components/AutoDarkMode.astro
Normal file
4
src/components/AutoDarkMode.astro
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline>!function(){var a="dark",b="auto",c="colorScheme",d=document.body.classList,e=localStorage,f=e.getItem(c);f&&f!==b?f===a&&d.add(a):(e.setItem(c,b),matchMedia("(prefers-color-scheme: dark)").matches&&d.add(a))}();</script>
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
|
import DarkModeInline from "./AutoDarkMode.astro";
|
||||||
|
---
|
||||||
|
|
||||||
---
|
<DarkModeInline />
|
||||||
|
|
||||||
<script is:inline>!function(){var a="dark",b="auto",c="colorScheme",d=document.body.classList,e=localStorage,f=e.getItem(c);f&&f!==b?f===a&&d.add(a):(e.setItem(c,b),matchMedia("(prefers-color-scheme: dark)").matches&&d.add(a))}();</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
@ -12,6 +12,7 @@
|
||||||
}
|
}
|
||||||
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
|
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
|
||||||
button.classList.remove("hidden");
|
button.classList.remove("hidden");
|
||||||
|
button.style.removeProperty("display");
|
||||||
button.setAttribute("aria-hidden", "false");
|
button.setAttribute("aria-hidden", "false");
|
||||||
button.addEventListener("click", (e) => {
|
button.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -13,55 +13,70 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="comment-section"
|
id="comments-section"
|
||||||
class="px-2 font-serif"
|
class="px-2 font-serif"
|
||||||
aria-describedby="title-comment-section"
|
aria-describedby="title-comments-section"
|
||||||
data-link={link}
|
data-link={link}
|
||||||
data-instance={instance}
|
data-instance={instance}
|
||||||
data-user={user}
|
data-user={user}
|
||||||
data-post-id={postId}
|
data-post-id={postId}
|
||||||
>
|
>
|
||||||
<h2 id="title-comment-section" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-comments-section" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
Comments
|
Comments
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-stone-800 dark:text-stone-100" id="comments">
|
<p id="comments-description" class="my-1 text-stone-800 dark:text-stone-100">
|
||||||
<p class="my-1">
|
<span data-noscript
|
||||||
<a class="text-link underline" href={link} target="_blank">View comments on Mastodon</a>.
|
><a class="u-syndication text-link underline" href={link} target="_blank">View comments on Mastodon</a>.</span
|
||||||
</p>
|
>
|
||||||
</div>
|
<span style={{ display: "none" }} data-no-comments
|
||||||
</section>
|
>No comments yet. <a class="text-link underline" href={link} target="_blank"
|
||||||
|
>Be the first to join the conversation on Mastodon</a
|
||||||
<template id="template-button">
|
>.</span
|
||||||
|
>
|
||||||
|
<span style={{ display: "none" }} data-comments
|
||||||
|
>Join the conversation <a class="text-link underline" href={link} target="_blank">by replying on Mastodon</a
|
||||||
|
>.</span
|
||||||
|
>
|
||||||
|
<span style={{ display: "none" }} data-error>Unable to load comments. Please try again later.</span>
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
class="mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 underline disabled:bg-bm-400 disabled:no-underline dark:bg-green-800 dark:disabled:bg-green-900"
|
style={{ display: "none" }}
|
||||||
|
class="group mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 text-stone-800 disabled:bg-bm-400 dark:bg-green-800 dark:text-stone-100 dark:disabled:bg-green-900"
|
||||||
id="load-comments-button"
|
id="load-comments-button"
|
||||||
>
|
>
|
||||||
<span>Click to load comments</span>
|
<span class="block underline group-disabled:hidden">Click to load comments</span>
|
||||||
|
<span class="hidden group-disabled:block">
|
||||||
|
<svg
|
||||||
|
style={{ width: "1.25rem", height: "1.25rem", display: "inline" }}
|
||||||
|
class="-mt-1 mr-1 animate-spin"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-100"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
<div id="comments" style={{ display: "none" }}></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<template id="template-button-loading">
|
<template id="template-comment-emoji">
|
||||||
<svg
|
<picture>
|
||||||
style={{ width: "1.25rem", height: "1.25rem", display: "inline" }}
|
<source media="(prefers-reduced-motion: reduce)" />
|
||||||
class="-mt-1 mr-1 animate-spin"
|
<img width={20} class="mx-[1px] inline" />
|
||||||
fill="none"
|
</picture>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path
|
|
||||||
class="opacity-100"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span>Loading...</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="template-comment-box">
|
<template id="template-comment-box">
|
||||||
<div
|
<div
|
||||||
role="article"
|
role="article"
|
||||||
class="p-comment h-entry my-2 rounded-md border-2 border-stone-400 bg-stone-100 p-2 dark:border-stone-600 dark:bg-stone-800"
|
class="p-comment h-entry my-2 rounded-md border-2 border-stone-400 bg-stone-100 p-2 text-stone-800 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-100"
|
||||||
>
|
>
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
<a
|
<a
|
||||||
|
@ -69,18 +84,27 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
class="p-author h-card u-url text-link flex items-center text-lg hover:underline focus:underline"
|
class="p-author h-card u-url text-link flex items-center text-lg hover:underline focus:underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img data-avatar class="u-photo mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" />
|
<picture>
|
||||||
<span data-display-name class="p-nickname"></span>
|
<source data-avatar-static media="(prefers-reduced-motion: reduce)" />
|
||||||
|
<img data-avatar width={40} class="u-photo mr-2 rounded-full border border-stone-400 dark:border-stone-600" />
|
||||||
|
</picture>
|
||||||
|
<span data-display-name class="p-nickname" aria-label="Display name"></span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
data-post-link
|
data-post-link
|
||||||
class="u-url text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
|
class="u-url text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<time class="dt-published mr-1" data-publish-date aria-label="Publish date"></time>
|
<time class="dt-published mr-1" data-published-date aria-label="Publish date"></time>
|
||||||
|
<time style={{ display: "none" }} class="italic" data-edited-date>(edited)</time>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div data-content class="e-content prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div>
|
<div
|
||||||
|
data-content
|
||||||
|
class="e-content prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"
|
||||||
|
aria-label="Comment body"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div class="ml-1 flex flex-row pb-2 pt-1">
|
<div class="ml-1 flex flex-row pb-2 pt-1">
|
||||||
<div class="flex" aria-label="Favorites">
|
<div class="flex" aria-label="Favorites">
|
||||||
<span data-favorites></span>
|
<span data-favorites></span>
|
||||||
|
@ -99,25 +123,25 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-comment-thread class="-mb-2" aria-hidden="true"></div>
|
<div data-comment-thread class="-mb-2" aria-hidden="true" aria-label="Replies"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
interface Post {
|
interface MastodonPost {
|
||||||
link: string;
|
link: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
user: string;
|
user: string;
|
||||||
postId: string;
|
postId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emoji {
|
interface CustomEmoji {
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
url: string;
|
url: string;
|
||||||
static_url: string;
|
static_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Comment {
|
interface Status {
|
||||||
id: string;
|
id: string;
|
||||||
in_reply_to_id: string;
|
in_reply_to_id: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -132,85 +156,107 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
url: string;
|
url: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
avatar_static: string;
|
avatar_static: string;
|
||||||
emojis: Emoji[];
|
emojis: CustomEmoji[];
|
||||||
};
|
};
|
||||||
content: string;
|
content: string;
|
||||||
emojis: Emoji[];
|
emojis: CustomEmoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiResponse {
|
interface Context {
|
||||||
ancestors: Comment[];
|
ancestors: Status[];
|
||||||
descendants: Comment[];
|
descendants: Status[];
|
||||||
}
|
}
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
function replaceEmojis(text: string, emojis: Emoji[]) {
|
async function renderComments(section: Element, post: MastodonPost) {
|
||||||
return emojis.reduce(
|
const commentsDescription = section.querySelector<HTMLElementTagNameMap["p"]>("p#comments-description")!;
|
||||||
(acc, emoji) =>
|
const loadCommentsButton = section.querySelector<HTMLElementTagNameMap["button"]>("button#load-comments-button")!;
|
||||||
acc.replaceAll(
|
|
||||||
`:${emoji.shortcode}:`,
|
|
||||||
`<img class="inline mx-[1px] w-5" alt=":${emoji.shortcode}: emoji" src="${emoji.url}" />`,
|
|
||||||
),
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderComments(section: Element, post: Post) {
|
|
||||||
const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
|
const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
||||||
}
|
}
|
||||||
const data: ApiResponse = await response.json();
|
const data: Context = await response.json();
|
||||||
|
|
||||||
const commentsList: HTMLElement[] = [];
|
const emojiTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
|
||||||
|
"template#template-comment-emoji",
|
||||||
|
)!;
|
||||||
|
const emojiMap: Record<string, string> = {};
|
||||||
|
const replaceEmojis = (text: string, emojis: CustomEmoji[]) =>
|
||||||
|
emojis.reduce((acc, emoji) => {
|
||||||
|
let emojiHTML = emojiMap[emoji.url];
|
||||||
|
if (!emojiHTML) {
|
||||||
|
const emojiPicture = emojiTemplate.content.cloneNode(true) as DocumentFragment;
|
||||||
|
const emojiStatic = emojiPicture.querySelector("source")!;
|
||||||
|
emojiStatic.srcset = emoji.static_url;
|
||||||
|
const emojiImg = emojiPicture.querySelector("img")!;
|
||||||
|
emojiImg.src = emoji.url;
|
||||||
|
emojiImg.alt = `:${emoji.shortcode}: emoji`;
|
||||||
|
emojiHTML = emojiPicture.firstElementChild!.outerHTML;
|
||||||
|
emojiMap[emoji.url] = emojiHTML;
|
||||||
|
}
|
||||||
|
return acc.replaceAll(`:${emoji.shortcode}:`, emojiHTML);
|
||||||
|
}, text);
|
||||||
|
const commentsList: DocumentFragment[] = [];
|
||||||
const commentMap: Record<string, number> = {};
|
const commentMap: Record<string, number> = {};
|
||||||
const commentTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
|
const commentTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
|
||||||
"template#template-comment-box",
|
"template#template-comment-box",
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
data.descendants.forEach((comment) => {
|
data.descendants.forEach((comment) => {
|
||||||
const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
|
const commentBox = commentTemplate.content.cloneNode(true) as DocumentFragment;
|
||||||
commentBox.id = `comment-${comment.id}`;
|
commentBox.firstElementChild!.id = `comment-${comment.id}`;
|
||||||
|
|
||||||
const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
|
const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
|
||||||
commentBoxAuthor.href = comment.account.url;
|
commentBoxAuthor.href = comment.account.url;
|
||||||
|
commentBoxAuthor.title = comment.account.acct;
|
||||||
|
commentBoxAuthor.setAttribute("aria-label", comment.account.acct);
|
||||||
const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!;
|
const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!;
|
||||||
avatar.src = comment.account.avatar;
|
avatar.src = comment.account.avatar;
|
||||||
avatar.alt = `Avatar of ${comment.account.username}`;
|
avatar.alt = `Avatar of @${comment.account.acct}`;
|
||||||
|
const avatarStatic =
|
||||||
|
commentBoxAuthor.querySelector<HTMLElementTagNameMap["source"]>("source[data-avatar-static]")!;
|
||||||
|
avatarStatic.srcset = comment.account.avatar_static;
|
||||||
const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!;
|
const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!;
|
||||||
displayName.innerHTML = replaceEmojis(comment.account.display_name, comment.account.emojis);
|
displayName.insertAdjacentHTML(
|
||||||
|
"afterbegin",
|
||||||
|
replaceEmojis(comment.account.display_name, comment.account.emojis),
|
||||||
|
);
|
||||||
|
|
||||||
const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
|
const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
|
||||||
commentBoxPostLink.href = comment.url;
|
commentBoxPostLink.href = comment.url;
|
||||||
const publishDate =
|
const publishDate =
|
||||||
commentBoxPostLink.querySelector<HTMLElementTagNameMap["time"]>("time[data-publish-date]")!;
|
commentBoxPostLink.querySelector<HTMLElementTagNameMap["time"]>("time[data-published-date]")!;
|
||||||
publishDate.setAttribute("datetime", comment.created_at);
|
publishDate.dateTime = comment.created_at;
|
||||||
publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
|
publishDate.insertAdjacentText(
|
||||||
month: "short",
|
"afterbegin",
|
||||||
day: "numeric",
|
new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
|
||||||
year: "numeric",
|
month: "short",
|
||||||
hour: "2-digit",
|
day: "numeric",
|
||||||
minute: "2-digit",
|
year: "numeric",
|
||||||
});
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (comment.edited_at) {
|
if (comment.edited_at) {
|
||||||
const edited = document.createElement("time");
|
const edited = commentBoxPostLink.querySelector<HTMLElementTagNameMap["time"]>("time[data-edited-date]")!;
|
||||||
edited.className = "dt-updated italic";
|
edited.dateTime = comment.edited_at;
|
||||||
edited.setAttribute("datetime", comment.edited_at);
|
edited.title = comment.edited_at;
|
||||||
edited.setAttribute("title", comment.edited_at);
|
edited.classList.remove("hidden");
|
||||||
edited.innerText = "(edited)";
|
edited.classList.add("dt-updated");
|
||||||
commentBoxPostLink.appendChild(edited);
|
edited.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentBoxContent = commentBox.querySelector<HTMLElementTagNameMap["div"]>("div[data-content]")!;
|
const commentBoxContent = commentBox.querySelector<HTMLElementTagNameMap["div"]>("div[data-content]")!;
|
||||||
commentBoxContent.innerHTML = replaceEmojis(comment.content, comment.emojis);
|
commentBoxContent.insertAdjacentHTML("afterbegin", replaceEmojis(comment.content, comment.emojis));
|
||||||
|
|
||||||
const commentBoxFavorites = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-favorites]")!;
|
const commentBoxFavorites = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-favorites]")!;
|
||||||
commentBoxFavorites.innerText = comment.favourites_count.toString();
|
commentBoxFavorites.insertAdjacentText("afterbegin", comment.favourites_count.toString());
|
||||||
|
|
||||||
const commentBoxReblogs = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-reblogs]")!;
|
const commentBoxReblogs = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-reblogs]")!;
|
||||||
commentBoxReblogs.innerText = comment.reblogs_count.toString();
|
commentBoxReblogs.insertAdjacentText("afterbegin", comment.reblogs_count.toString());
|
||||||
|
|
||||||
if (comment.in_reply_to_id === post.postId || !(comment.in_reply_to_id in commentMap)) {
|
if (comment.in_reply_to_id === post.postId || !(comment.in_reply_to_id in commentMap)) {
|
||||||
commentMap[comment.id] = commentsList.length;
|
commentMap[comment.id] = commentsList.length;
|
||||||
|
@ -221,24 +267,36 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
const parentThreadDiv =
|
const parentThreadDiv =
|
||||||
commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
|
commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
|
||||||
parentThreadDiv.setAttribute("aria-hidden", "false");
|
parentThreadDiv.setAttribute("aria-hidden", "false");
|
||||||
parentThreadDiv.setAttribute("aria-label", "Replies");
|
|
||||||
parentThreadDiv.appendChild(commentBox);
|
parentThreadDiv.appendChild(commentBox);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (commentsList.length === 0) {
|
if (commentsList.length) {
|
||||||
commentsDiv.innerHTML = `<p class="my-1">No comments yet. <a class="text-link underline" href="${post.link}" target="_blank">Be the first to join the conversation on Mastodon</a>.</p>`;
|
const fragment = document.createDocumentFragment();
|
||||||
|
commentsList.forEach((comment) => fragment.appendChild(comment));
|
||||||
|
commentsDiv.appendChild(fragment);
|
||||||
|
commentsDescription
|
||||||
|
.querySelector<HTMLElementTagNameMap["span"]>("span[data-comments]")!
|
||||||
|
.style.removeProperty("display");
|
||||||
|
commentsDiv.style.removeProperty("display");
|
||||||
} else {
|
} else {
|
||||||
commentsDiv.innerHTML = `<p class="my-1">Join the conversation <a class="text-link underline" href="${post.link}" target="_blank">by replying on Mastodon</a>.</p>`;
|
commentsDescription
|
||||||
commentsDiv.append(...commentsList);
|
.querySelector<HTMLElementTagNameMap["span"]>("span[data-no-comments]")!
|
||||||
|
.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
commentsDiv.innerHTML = `<p class="my-1">Unable to load comments. Please try again later.</p>`;
|
|
||||||
console.error("Fetch Mastodon comments error", e);
|
console.error("Fetch Mastodon comments error", e);
|
||||||
|
commentsDescription
|
||||||
|
.querySelector<HTMLElementTagNameMap["span"]>("span[data-error]")!
|
||||||
|
.style.removeProperty("display");
|
||||||
|
} finally {
|
||||||
|
loadCommentsButton.style.display = "none";
|
||||||
|
loadCommentsButton.blur();
|
||||||
|
commentsDescription.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initCommentSection() {
|
function initCommentSection() {
|
||||||
const commentSection = document.querySelector<HTMLElementTagNameMap["section"]>("section#comment-section");
|
const commentSection = document.querySelector<HTMLElementTagNameMap["section"]>("section#comments-section");
|
||||||
if (!commentSection) {
|
if (!commentSection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -251,21 +309,21 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
if (!post.link || !post.instance || !post.user || !post.postId) {
|
if (!post.link || !post.instance || !post.user || !post.postId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const commentsContainer = commentSection.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
const loadCommentsButton =
|
||||||
commentsContainer.replaceChildren(
|
commentSection.querySelector<HTMLElementTagNameMap["button"]>("button#load-comments-button")!;
|
||||||
document.querySelector<HTMLElementTagNameMap["template"]>("template#template-button")!.content.cloneNode(true),
|
loadCommentsButton.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
loadCommentsButton.disabled = true;
|
||||||
|
renderComments(commentSection, post as MastodonPost);
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
);
|
);
|
||||||
const loadCommentsButton = commentsContainer.querySelector("button")!;
|
const commentsDescription = commentSection.querySelector<HTMLElementTagNameMap["p"]>("p#comments-description")!;
|
||||||
loadCommentsButton.addEventListener("click", (e) => {
|
commentsDescription.style.display = "none";
|
||||||
e.preventDefault();
|
commentsDescription.querySelector<HTMLElementTagNameMap["span"]>("span[data-noscript]")!.style.display = "none";
|
||||||
loadCommentsButton.setAttribute("disabled", "true");
|
loadCommentsButton.style.removeProperty("display");
|
||||||
loadCommentsButton.replaceChildren(
|
|
||||||
document
|
|
||||||
.querySelector<HTMLElementTagNameMap["template"]>("template#template-button-loading")!
|
|
||||||
.content.cloneNode(true),
|
|
||||||
);
|
|
||||||
renderComments(commentSection, post as Post);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initCommentSection();
|
initCommentSection();
|
||||||
|
|
|
@ -133,7 +133,7 @@ const userList = z
|
||||||
const copyrightedCharacters = z
|
const copyrightedCharacters = z
|
||||||
.record(z.string(), reference("users"))
|
.record(z.string(), reference("users"))
|
||||||
.refine(
|
.refine(
|
||||||
(value) => !("" in value) || Object.keys(value).length == 1,
|
(value) => !("" in value) || Object.keys(value).length === 1,
|
||||||
`"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
|
`"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
|
||||||
)
|
)
|
||||||
.default({});
|
.default({});
|
||||||
|
@ -145,7 +145,7 @@ const publishedContent = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
authors: userList,
|
authors: userList,
|
||||||
contentWarning: z.string().trim(),
|
contentWarning: z.string().trim(),
|
||||||
// Required parameters, but optional for drafts (isDraft == true)
|
// Required parameters, but optional for drafts (isDraft === true)
|
||||||
pubDate: z
|
pubDate: z
|
||||||
.date()
|
.date()
|
||||||
.transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
|
.transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
|
||||||
|
@ -230,7 +230,7 @@ const storiesCollection = defineCollection({
|
||||||
schema: ({ image }) =>
|
schema: ({ image }) =>
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
// Required parameters, but optional for drafts (isDraft == true)
|
// Required parameters, but optional for drafts (isDraft === true)
|
||||||
wordCount: z.number().int().optional(),
|
wordCount: z.number().int().optional(),
|
||||||
thumbnail: image().optional(),
|
thumbnail: image().optional(),
|
||||||
// Optional parameters
|
// Optional parameters
|
||||||
|
@ -260,7 +260,7 @@ const gamesCollection = defineCollection({
|
||||||
schema: ({ image }) =>
|
schema: ({ image }) =>
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
// Required parameters, but optional for drafts (isDraft == true)
|
// Required parameters, but optional for drafts (isDraft === true)
|
||||||
platforms: z.array(platform).default([]),
|
platforms: z.array(platform).default([]),
|
||||||
thumbnail: image().optional(),
|
thumbnail: image().optional(),
|
||||||
// Optional parameters
|
// Optional parameters
|
||||||
|
|
|
@ -7,7 +7,7 @@ const UI_STRINGS = {
|
||||||
en: (names: string[]) =>
|
en: (names: string[]) =>
|
||||||
names.length <= 1
|
names.length <= 1
|
||||||
? names.join("")
|
? names.join("")
|
||||||
: names.length == 2
|
: names.length === 2
|
||||||
? `${names[0]} and ${names[1]}`
|
? `${names[0]} and ${names[1]}`
|
||||||
: `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`,
|
: `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`,
|
||||||
tok: (names: string[]) => names.join(" en "),
|
tok: (names: string[]) => names.join(" en "),
|
||||||
|
@ -17,10 +17,10 @@ const UI_STRINGS = {
|
||||||
},
|
},
|
||||||
"util/enumerate": {
|
"util/enumerate": {
|
||||||
en: (count: number, nounSingular: string, nounPlural?: string) => {
|
en: (count: number, nounSingular: string, nounPlural?: string) => {
|
||||||
if (count == 0) {
|
if (count === 0) {
|
||||||
return `no ${nounPlural ?? nounSingular}`;
|
return `no ${nounPlural ?? nounSingular}`;
|
||||||
}
|
}
|
||||||
if (count == 1) {
|
if (count === 1) {
|
||||||
return `one ${nounSingular}`;
|
return `one ${nounSingular}`;
|
||||||
}
|
}
|
||||||
return `${count} ${nounPlural ?? nounSingular}`;
|
return `${count} ${nounPlural ?? nounSingular}`;
|
||||||
|
@ -123,14 +123,14 @@ const UI_STRINGS = {
|
||||||
tok: "nimi lipu",
|
tok: "nimi lipu",
|
||||||
},
|
},
|
||||||
"story/authors_aria_label": {
|
"story/authors_aria_label": {
|
||||||
en: (authors: any[]) => (authors.length == 1 ? "Author" : "Authors"),
|
en: (authors: any[]) => (authors.length === 1 ? "Author" : "Authors"),
|
||||||
tok: (_authors: any[]) => "jan pi pali lipu",
|
tok: (_authors: any[]) => "jan pi pali lipu",
|
||||||
},
|
},
|
||||||
"story/requesters_aria_label": {
|
"story/requesters_aria_label": {
|
||||||
en: (requesters: any[]) => (requesters.length == 1 ? "Requester" : "Requesters"),
|
en: (requesters: any[]) => (requesters.length === 1 ? "Requester" : "Requesters"),
|
||||||
},
|
},
|
||||||
"story/commissioners_aria_label": {
|
"story/commissioners_aria_label": {
|
||||||
en: (commissioners: any[]) => (commissioners.length == 1 ? "Commissioner" : "Commissioners"),
|
en: (commissioners: any[]) => (commissioners.length === 1 ? "Commissioner" : "Commissioners"),
|
||||||
},
|
},
|
||||||
"story/warnings": {
|
"story/warnings": {
|
||||||
en: (wordCount: number | string | undefined, contentWarning: string) =>
|
en: (wordCount: number | string | undefined, contentWarning: string) =>
|
||||||
|
@ -182,7 +182,7 @@ const UI_STRINGS = {
|
||||||
},
|
},
|
||||||
"game/platforms": {
|
"game/platforms": {
|
||||||
en: (platforms: GamePlatform[]) => {
|
en: (platforms: GamePlatform[]) => {
|
||||||
if (platforms.length == 0) {
|
if (platforms.length === 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const translatedPlatforms = platforms.map((platform) => {
|
const translatedPlatforms = platforms.map((platform) => {
|
||||||
|
@ -238,9 +238,9 @@ const UI_STRINGS = {
|
||||||
},
|
},
|
||||||
"characters/characters_are_copyrighted_by": {
|
"characters/characters_are_copyrighted_by": {
|
||||||
en: (owner: string, charactersList: string[]) =>
|
en: (owner: string, charactersList: string[]) =>
|
||||||
charactersList.length == 0
|
charactersList.length === 0
|
||||||
? `All characters are © ${owner}`
|
? `All characters are © ${owner}`
|
||||||
: charactersList.length == 1
|
: charactersList.length === 1
|
||||||
? `${charactersList[0]} is © ${owner}`
|
? `${charactersList[0]} is © ${owner}`
|
||||||
: `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
|
: `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
|
||||||
},
|
},
|
||||||
|
@ -254,7 +254,7 @@ const UI_STRINGS = {
|
||||||
if (gamesCount > 0) {
|
if (gamesCount > 0) {
|
||||||
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
|
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
|
||||||
}
|
}
|
||||||
if (content.length == 0) {
|
if (content.length === 0) {
|
||||||
return `No works tagged with "${tag}".`;
|
return `No works tagged with "${tag}".`;
|
||||||
}
|
}
|
||||||
return UI_STRINGS["util/capitalize"].en(`${UI_STRINGS["util/join_names"].en(content)} tagged with "${tag}".`);
|
return UI_STRINGS["util/capitalize"].en(`${UI_STRINGS["util/join_names"].en(content)} tagged with "${tag}".`);
|
||||||
|
|
|
@ -42,7 +42,7 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
<div class="pt-4 text-center text-xs text-black dark:text-white">
|
<div class="pt-4 text-center text-xs text-black dark:text-white">
|
||||||
<span
|
<span
|
||||||
>© {
|
>© {
|
||||||
currentYear == "2024" ? (
|
currentYear === "2024" ? (
|
||||||
<time datetime="2024">2024</time>
|
<time datetime="2024">2024</time>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -64,7 +64,7 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
|
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span id="label-main-website" class="hidden">Main website</span>
|
<span id="label-main-website" class="sr-only">Main website</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
|
<a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
|
||||||
<svg
|
<svg
|
||||||
|
@ -76,9 +76,14 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
|
d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span id="label-rss-feed" class="hidden">RSS feed</span>
|
<span id="label-rss-feed" class="sr-only">RSS feed</span>
|
||||||
</a>
|
</a>
|
||||||
<button data-dark-mode class="text-link hidden p-1" aria-labelledby="label-toggle-dark-mode" aria-hidden="true">
|
<button
|
||||||
|
data-dark-mode
|
||||||
|
style={{ display: "none" }}
|
||||||
|
class="text-link p-1"
|
||||||
|
aria-labelledby="label-toggle-dark-mode"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
||||||
viewBox="0 0 512 512"
|
viewBox="0 0 512 512"
|
||||||
|
@ -99,7 +104,7 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span id="label-toggle-dark-mode" class="hidden">{t("en", "published_content/toggle_dark_mode")}</span>
|
<span id="label-toggle-dark-mode" class="sr-only">{t("en", "published_content/toggle_dark_mode")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -126,7 +126,7 @@ const thumbnail =
|
||||||
d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
|
d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden" id="label-return-to"
|
<span class="sr-only" id="label-return-to"
|
||||||
>{
|
>{
|
||||||
series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title
|
series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title
|
||||||
}</span
|
}</span
|
||||||
|
@ -146,14 +146,15 @@ const thumbnail =
|
||||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
|
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden" id="label-go-to-description">{t(props.lang, "published_content/go_to_description")}</span
|
<span class="sr-only" id="label-go-to-description"
|
||||||
|
>{t(props.lang, "published_content/go_to_description")}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
data-dark-mode
|
data-dark-mode
|
||||||
class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
|
style={{ display: "none" }}
|
||||||
|
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-labelledby="label-toggle-dark-mode"
|
aria-labelledby="label-toggle-dark-mode"
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||||
|
@ -175,7 +176,7 @@ const thumbnail =
|
||||||
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
<span class="sr-only" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
commissionersList.map((commissioner) => u(commissioner)),
|
commissionersList.map((commissioner) => u(commissioner)),
|
||||||
),
|
),
|
||||||
...copyrightedCharacters.map(({ user, characters }) =>
|
...copyrightedCharacters.map(({ user, characters }) =>
|
||||||
t(lang, "characters/characters_are_copyrighted_by", u(user), characters[0] == "" ? [] : characters),
|
t(lang, "characters/characters_are_copyrighted_by", u(user), characters[0] === "" ? [] : characters),
|
||||||
),
|
),
|
||||||
].reduce(async (promise, data) => {
|
].reduce(async (promise, data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
|
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
|
||||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
import { DEFAULT_LANG, t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import UserComponent from "../components/UserComponent.astro";
|
import UserComponent from "../components/UserComponent.astro";
|
||||||
|
|
||||||
type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } };
|
type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } };
|
||||||
|
@ -24,7 +24,7 @@ const games = await Promise.all(
|
||||||
<ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{
|
{
|
||||||
games.map((game, i) => (
|
games.map((game, i) => (
|
||||||
<li class="h-entry">
|
<li class="h-entry" lang={game.data.lang}>
|
||||||
<a
|
<a
|
||||||
class="u-url text-link hover:underline focus:underline"
|
class="u-url text-link hover:underline focus:underline"
|
||||||
href={`/games/${game.slug}`}
|
href={`/games/${game.slug}`}
|
||||||
|
@ -42,17 +42,31 @@ const games = await Promise.all(
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-[288px] text-sm">
|
<div class="max-w-[288px] text-sm">
|
||||||
<span class="p-name">{game.data.title}</span>
|
<span class="p-name" aria-label="Title">
|
||||||
|
{game.data.title}
|
||||||
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<time class="dt-published italic" datetime={game.data.pubDate.toISOString().slice(0, 10)}>
|
<time
|
||||||
|
class="dt-published italic"
|
||||||
|
datetime={game.data.pubDate.toISOString().slice(0, 10)}
|
||||||
|
aria-label="Publish date"
|
||||||
|
>
|
||||||
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div style={{ display: "none" }}>
|
<div class="sr-only">
|
||||||
{game.authors.map((author) => (
|
<p class="p-category" aria-label="Category">
|
||||||
<UserComponent rel="author" class="p-author" user={author} lang={DEFAULT_LANG} />
|
Game
|
||||||
))}
|
</p>
|
||||||
|
<p class="p-summary" aria-label="Summary">
|
||||||
|
{t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
|
||||||
|
</p>
|
||||||
|
<div aria-label="Authors">
|
||||||
|
{game.authors.map((author) => (
|
||||||
|
<UserComponent rel="author" class="p-author" user={author} lang={game.data.lang} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { ImageMetadata } from "astro";
|
||||||
import { type CollectionEntry, type CollectionKey, getCollection, getEntries } from "astro:content";
|
import { type CollectionEntry, type CollectionKey, getCollection, getEntries } from "astro:content";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
import { DEFAULT_LANG, t, type Lang } from "../i18n";
|
import { t, type Lang } from "../i18n";
|
||||||
import UserComponent from "../components/UserComponent.astro";
|
import UserComponent from "../components/UserComponent.astro";
|
||||||
|
|
||||||
const MAX_ITEMS = 10;
|
const MAX_ITEMS = 10;
|
||||||
|
@ -60,7 +60,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
|
||||||
href: `/games/${game.slug}`,
|
href: `/games/${game.slug}`,
|
||||||
title: game.data.title,
|
title: game.data.title,
|
||||||
authors: await getEntries(game.data.authors),
|
authors: await getEntries(game.data.authors),
|
||||||
lang: DEFAULT_LANG,
|
lang: game.data.lang,
|
||||||
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning),
|
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning),
|
||||||
pubDate: game.data.pubDate,
|
pubDate: game.data.pubDate,
|
||||||
}) satisfies LatestItemsEntry,
|
}) satisfies LatestItemsEntry,
|
||||||
|
@ -99,7 +99,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{
|
{
|
||||||
latestItems.map((entry) => (
|
latestItems.map((entry) => (
|
||||||
<li class="h-entry break-inside-avoid">
|
<li class="h-entry break-inside-avoid" lang={entry.lang}>
|
||||||
<a class="u-url text-link hover:underline focus:underline" href={entry.href} title={entry.altText}>
|
<a class="u-url text-link hover:underline focus:underline" href={entry.href} title={entry.altText}>
|
||||||
{entry.thumbnail ? (
|
{entry.thumbnail ? (
|
||||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||||
|
@ -113,20 +113,34 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-[192px] text-sm">
|
<div class="max-w-[192px] text-sm">
|
||||||
<span class="p-name">{entry.title}</span>
|
<span class="p-name" aria-label="Title">
|
||||||
|
{entry.title}
|
||||||
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="italic">
|
<span class="italic">
|
||||||
<span class="p-category">{entry.type}</span> –{" "}
|
<span class="p-category" aria-label="Category">
|
||||||
<time class="dt-published" datetime={entry.pubDate.toISOString().slice(0, 10)}>
|
{entry.type}
|
||||||
|
</span>{" "}
|
||||||
|
–{" "}
|
||||||
|
<time
|
||||||
|
class="dt-published"
|
||||||
|
datetime={entry.pubDate.toISOString().slice(0, 10)}
|
||||||
|
aria-label="Publish date"
|
||||||
|
>
|
||||||
{entry.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
{entry.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||||
</time>
|
</time>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div style={{ display: "none" }}>
|
<div class="sr-only">
|
||||||
{entry.authors.map((author) => (
|
<p class="p-summary" aria-label="Summary">
|
||||||
<UserComponent rel="author" class="p-author" user={author} lang={entry.lang} />
|
{entry.altText}
|
||||||
))}
|
</p>
|
||||||
|
<div aria-label="Authors">
|
||||||
|
{entry.authors.map((author) => (
|
||||||
|
<UserComponent rel="author" class="p-author" user={author} lang={entry.lang} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
|
|
|
@ -35,7 +35,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
<p class="my-4">The bulk of my content!</p>
|
<p class="my-4">The bulk of my content!</p>
|
||||||
<p class="text-center font-light text-stone-950 dark:text-white">
|
<p class="text-center font-light text-stone-950 dark:text-white">
|
||||||
{
|
{
|
||||||
page.start == page.end
|
page.start === page.end
|
||||||
? `Displaying story #${page.start + 1}`
|
? `Displaying story #${page.start + 1}`
|
||||||
: `Displaying stories #${page.start + 1}–${page.end + 1}`
|
: `Displaying stories #${page.start + 1}–${page.end + 1}`
|
||||||
} / {page.total}
|
} / {page.total}
|
||||||
|
@ -53,7 +53,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
[...Array(totalPages).keys()]
|
[...Array(totalPages).keys()]
|
||||||
.map((p) => p + 1)
|
.map((p) => p + 1)
|
||||||
.map((p) =>
|
.map((p) =>
|
||||||
p == page.currentPage ? (
|
p === page.currentPage ? (
|
||||||
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
|
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
|
||||||
{p}
|
{p}
|
||||||
</span>
|
</span>
|
||||||
|
@ -78,7 +78,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{
|
{
|
||||||
page.data.map((story, i) => (
|
page.data.map((story, i) => (
|
||||||
<li class="h-entry break-inside-avoid">
|
<li class="h-entry break-inside-avoid" lang={story.data.lang}>
|
||||||
<a
|
<a
|
||||||
class="u-url text-link hover:underline focus:underline"
|
class="u-url text-link hover:underline focus:underline"
|
||||||
href={`/stories/${story.slug}`}
|
href={`/stories/${story.slug}`}
|
||||||
|
@ -96,17 +96,31 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-[192px] text-sm">
|
<div class="max-w-[192px] text-sm">
|
||||||
<span class="p-name">{story.data.title}</span>
|
<span class="p-name" aria-label="Title">
|
||||||
|
{story.data.title}
|
||||||
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<time class="italic" datetime={story.data.pubDate.toISOString().slice(0, 10)}>
|
<time
|
||||||
|
class="dt-published italic"
|
||||||
|
datetime={story.data.pubDate.toISOString().slice(0, 10)}
|
||||||
|
aria-label="Publish date"
|
||||||
|
>
|
||||||
{story.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
{story.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div style={{ display: "none" }}>
|
<div class="sr-only">
|
||||||
{story.authors.map((author) => (
|
<p class="p-category" aria-label="Category">
|
||||||
<UserComponent rel="author" class="p-author" user={author} lang={story.data.lang} />
|
Story
|
||||||
))}
|
</p>
|
||||||
|
<p class="p-summary" aria-label="Summary">
|
||||||
|
{t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
|
||||||
|
</p>
|
||||||
|
<div aria-label="Authors">
|
||||||
|
{story.authors.map((author) => (
|
||||||
|
<UserComponent rel="author" class="p-author" user={author} lang={story.data.lang} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
|
@ -124,7 +138,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
[...Array(totalPages).keys()]
|
[...Array(totalPages).keys()]
|
||||||
.map((p) => p + 1)
|
.map((p) => p + 1)
|
||||||
.map((p) =>
|
.map((p) =>
|
||||||
p == page.currentPage ? (
|
p === page.currentPage ? (
|
||||||
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
|
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
|
||||||
{p}
|
{p}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -31,7 +31,7 @@ uncategorizedTagsSet.forEach((tag) => draftOnlyTagsSet.delete(tag));
|
||||||
const uniqueSlugs = new Set<string>();
|
const uniqueSlugs = new Set<string>();
|
||||||
const categorizedTags = tagCategories
|
const categorizedTags = tagCategories
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.data.index == b.data.index) {
|
if (a.data.index === b.data.index) {
|
||||||
throw new Error(`Found tag categories with same index value ${a.data.index} ("${a.id}", "${b.id}")`);
|
throw new Error(`Found tag categories with same index value ${a.data.index} ("${a.id}", "${b.id}")`);
|
||||||
}
|
}
|
||||||
return a.data.index - b.data.index;
|
return a.data.index - b.data.index;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
---
|
---
|
||||||
import type { GetStaticPaths } from "astro";
|
import type { GetStaticPaths } from "astro";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content";
|
import { type CollectionEntry, type CollectionKey, getCollection, getEntries } from "astro:content";
|
||||||
import { Markdown } from "@astropub/md";
|
import { Markdown } from "@astropub/md";
|
||||||
import { slug } from "github-slugger";
|
import { slug } from "github-slugger";
|
||||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||||
import Prose from "../../components/Prose.astro";
|
import Prose from "../../components/Prose.astro";
|
||||||
import { t, DEFAULT_LANG } from "../../i18n";
|
import { t, DEFAULT_LANG } from "../../i18n";
|
||||||
import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown";
|
import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown";
|
||||||
|
import UserComponent from "../../components/UserComponent.astro";
|
||||||
|
|
||||||
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
||||||
|
|
||||||
|
@ -15,8 +16,8 @@ type Props = {
|
||||||
tag: string;
|
tag: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
related?: string[];
|
related?: string[];
|
||||||
stories: EntryWithPubDate<"stories">[];
|
stories: (EntryWithPubDate<"stories"> & { authors: CollectionEntry<"users">[] })[];
|
||||||
games: EntryWithPubDate<"games">[];
|
games: (EntryWithPubDate<"games"> & { authors: CollectionEntry<"users">[] })[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
|
@ -46,7 +47,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
(acc, category) => {
|
(acc, category) => {
|
||||||
category.data.tags.forEach(({ name, description, related }) => {
|
category.data.tags.forEach(({ name, description, related }) => {
|
||||||
related = related.filter((relatedTag) => {
|
related = related.filter((relatedTag) => {
|
||||||
if (relatedTag == name) {
|
if (relatedTag === name) {
|
||||||
console.warn(`WARNING: Tag "${name}" should not have itself as a related tag; removing...`);
|
console.warn(`WARNING: Tag "${name}" should not have itself as a related tag; removing...`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -66,26 +67,42 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
},
|
},
|
||||||
{} as Record<string, { description?: string; related?: string[] }>,
|
{} as Record<string, { description?: string; related?: string[] }>,
|
||||||
);
|
);
|
||||||
return [...tags]
|
return await Promise.all(
|
||||||
.filter((tag) => !seriesTags.has(tag))
|
[...tags]
|
||||||
.map((tag) => ({
|
.filter((tag) => !seriesTags.has(tag))
|
||||||
params: { slug: slug(tag) } satisfies Params,
|
.map(async (tag) => ({
|
||||||
props: {
|
params: { slug: slug(tag) } satisfies Params,
|
||||||
tag,
|
props: {
|
||||||
description: tagDescriptions[tag]?.description,
|
tag,
|
||||||
related: tagDescriptions[tag]?.related,
|
description: tagDescriptions[tag]?.description,
|
||||||
stories: (
|
related: tagDescriptions[tag]?.related,
|
||||||
stories.filter(
|
stories: await Promise.all(
|
||||||
(story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag),
|
(
|
||||||
) as EntryWithPubDate<"stories">[]
|
stories.filter(
|
||||||
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()),
|
(story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag),
|
||||||
games: (
|
) as EntryWithPubDate<"stories">[]
|
||||||
games.filter(
|
)
|
||||||
(game) => !game.data.isDraft && game.data.pubDate && game.data.tags.includes(tag),
|
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||||
) as EntryWithPubDate<"games">[]
|
.map(async (story) => ({
|
||||||
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()),
|
...story,
|
||||||
} satisfies Props,
|
authors: await getEntries(story.data.authors),
|
||||||
}));
|
})),
|
||||||
|
),
|
||||||
|
games: await Promise.all(
|
||||||
|
(
|
||||||
|
games.filter(
|
||||||
|
(game) => !game.data.isDraft && game.data.pubDate && game.data.tags.includes(tag),
|
||||||
|
) as EntryWithPubDate<"games">[]
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||||
|
.map(async (game) => ({
|
||||||
|
...game,
|
||||||
|
authors: await getEntries(game.data.authors),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
} satisfies Props,
|
||||||
|
})),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props } = Astro;
|
const { props } = Astro;
|
||||||
|
@ -130,7 +147,7 @@ const totalWorksWithTag = t(
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{props.stories.map((story) => (
|
{props.stories.map((story) => (
|
||||||
<li class="h-entry break-inside-avoid">
|
<li class="h-entry break-inside-avoid" lang={story.data.lang}>
|
||||||
<a
|
<a
|
||||||
class="u-url text-link hover:underline focus:underline"
|
class="u-url text-link hover:underline focus:underline"
|
||||||
href={`/stories/${story.slug}`}
|
href={`/stories/${story.slug}`}
|
||||||
|
@ -147,9 +164,15 @@ const totalWorksWithTag = t(
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-[192px] text-sm">
|
<div class="max-w-[192px] text-sm">
|
||||||
<span class="p-name">{story.data.title}</span>
|
<span class="p-name" aria-label="Title">
|
||||||
|
{story.data.title}
|
||||||
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<time class="dt-published italic" datetime={story.data.pubDate.toISOString().slice(0, 10)}>
|
<time
|
||||||
|
class="dt-published italic"
|
||||||
|
datetime={story.data.pubDate.toISOString().slice(0, 10)}
|
||||||
|
aria-label="Publish date"
|
||||||
|
>
|
||||||
{story.data.pubDate.toLocaleDateString("en-US", {
|
{story.data.pubDate.toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
@ -158,6 +181,19 @@ const totalWorksWithTag = t(
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="sr-only">
|
||||||
|
<p class="p-category" aria-label="Category">
|
||||||
|
Story
|
||||||
|
</p>
|
||||||
|
<p class="p-summary" aria-label="Summary">
|
||||||
|
{t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
|
||||||
|
</p>
|
||||||
|
<div aria-label="Authors">
|
||||||
|
{story.authors.map((author) => (
|
||||||
|
<UserComponent rel="author" class="p-author" user={author} lang={story.data.lang} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -172,7 +208,7 @@ const totalWorksWithTag = t(
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{props.games.map((game) => (
|
{props.games.map((game) => (
|
||||||
<li class="h-entry break-inside-avoid">
|
<li class="h-entry break-inside-avoid" lang={game.data.lang}>
|
||||||
<a
|
<a
|
||||||
class="u-url text-link hover:underline focus:underline"
|
class="u-url text-link hover:underline focus:underline"
|
||||||
href={`/games/${game.slug}`}
|
href={`/games/${game.slug}`}
|
||||||
|
@ -189,13 +225,32 @@ const totalWorksWithTag = t(
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="max-w-[192px] text-sm">
|
<div class="max-w-[192px] text-sm">
|
||||||
<span class="p-name">{game.data.title}</span>
|
<span class="p-name" aria-label="Title">
|
||||||
|
{game.data.title}
|
||||||
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<time class="dt-published italic" datetime={game.data.pubDate.toISOString().slice(0, 10)}>
|
<time
|
||||||
|
class="dt-published italic"
|
||||||
|
datetime={game.data.pubDate.toISOString().slice(0, 10)}
|
||||||
|
aria-label="Publish date"
|
||||||
|
>
|
||||||
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="sr-only">
|
||||||
|
<p class="p-category" aria-label="Category">
|
||||||
|
Game
|
||||||
|
</p>
|
||||||
|
<p class="p-summary" aria-label="Summary">
|
||||||
|
{t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
|
||||||
|
</p>
|
||||||
|
<div aria-label="Authors">
|
||||||
|
{game.authors.map((author) => (
|
||||||
|
<UserComponent rel="author" class="p-author" user={author} lang={game.data.lang} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Add table
Reference in a new issue