Consolidate published content layouts and fix comments
This commit is contained in:
parent
d529b043c6
commit
c38275e2f2
19 changed files with 637 additions and 721 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"version": "1.6.3",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"version": "1.6.3",
|
||||
"version": "1.7.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.2",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"type": "module",
|
||||
"version": "1.6.3",
|
||||
"version": "1.7.0",
|
||||
"scripts": {
|
||||
"postinstall": "astro sync",
|
||||
"dev": "astro dev",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
>
|
||||
<div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
|
||||
<div class="text-bm-500 dark:text-bm-400">
|
||||
<svg width="3rem" height="3rem" class="fill-current" viewBox="0 0 512 512">
|
||||
<svg style={{ width: "3rem", height: "3rem", fill: "currentColor" }} viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
></path>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import type { Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
import { t, type Lang } from "../i18n";
|
||||
|
||||
type Props = {
|
||||
lang: Lang;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import type { Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
import { t, type Lang } from "../i18n";
|
||||
|
||||
type Props = {
|
||||
lang: Lang;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import type { CopyrightedCharacters, Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
import type { CopyrightedCharacters } from "../content/config";
|
||||
import { t, type Lang } from "../i18n";
|
||||
import UserComponent from "./UserComponent.astro";
|
||||
import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
|
||||
import { formatCopyrightedCharacters } from "../utils/format_copyrighted_characters";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import type { Lang } from "../content/config";
|
||||
import type { Lang } from "../i18n";
|
||||
|
||||
type Props = {
|
||||
lang: Lang;
|
||||
|
@ -42,9 +42,8 @@ const { link, instance, user, postId } = Astro.props;
|
|||
|
||||
<template id="template-button-loading">
|
||||
<svg
|
||||
width="1.25rem"
|
||||
height="1.25rem"
|
||||
class="-mt-1 mr-1 inline animate-spin"
|
||||
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
|
||||
|
@ -78,7 +77,7 @@ const { link, instance, user, postId } = Astro.props;
|
|||
<div class="ml-1 flex flex-row pb-2 pt-1">
|
||||
<div class="flex" aria-label="Favorites">
|
||||
<span data-favorites></span>
|
||||
<svg width="1.25rem" class="ml-2 fill-current" viewBox="0 0 576 512" aria-hidden>
|
||||
<svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 576 512" aria-hidden>
|
||||
<path
|
||||
d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
|
||||
></path>
|
||||
|
@ -86,7 +85,7 @@ const { link, instance, user, postId } = Astro.props;
|
|||
</div>
|
||||
<div class="ml-4 flex" aria-label="Reblogs">
|
||||
<span data-reblogs></span>
|
||||
<svg width="1.25rem" class="ml-2 fill-current" viewBox="0 0 512 512" aria-hidden>
|
||||
<svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 512 512" aria-hidden>
|
||||
<path
|
||||
d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"
|
||||
></path>
|
||||
|
@ -241,10 +240,11 @@ const { link, instance, user, postId } = Astro.props;
|
|||
if (!post.link || !post.instance || !post.user || !post.postId) {
|
||||
return;
|
||||
}
|
||||
const loadCommentsButton = document
|
||||
.querySelector<HTMLElementTagNameMap["template"]>("template#template-button")!
|
||||
.content.cloneNode(true) as HTMLButtonElement;
|
||||
commentSection.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!.replaceChildren(loadCommentsButton);
|
||||
const commentsContainer = commentSection.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
||||
commentsContainer.replaceChildren(
|
||||
document.querySelector<HTMLElementTagNameMap["template"]>("template#template-button")!.content.cloneNode(true),
|
||||
);
|
||||
const loadCommentsButton = commentsContainer.querySelector("button")!;
|
||||
loadCommentsButton.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
loadCommentsButton.setAttribute("disabled", "true");
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import type { Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
import { t, type Lang } from "../i18n";
|
||||
|
||||
type Props = {
|
||||
lang: Lang;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import type { Lang } from "../content/config";
|
||||
import type { Lang } from "../i18n";
|
||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -220,6 +220,7 @@ export type Lang = z.output<typeof lang>;
|
|||
export type Website = keyof z.input<typeof websiteLinks>;
|
||||
export type GamePlatform = z.infer<typeof platform>;
|
||||
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
||||
export type PublishedContent = z.infer<typeof publishedContent>;
|
||||
|
||||
// Content collections
|
||||
|
||||
|
@ -264,6 +265,8 @@ const gamesCollection = defineCollection({
|
|||
// Optional parameters
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
prev: reference("games").nullish(),
|
||||
next: reference("games").nullish(),
|
||||
})
|
||||
.and(publishedContent)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published game`)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { GamePlatform, Lang } from "../content/config";
|
||||
import { DEFAULT_LANG } from "../content/config";
|
||||
export { DEFAULT_LANG } from "../content/config";
|
||||
import { type GamePlatform, type Lang, DEFAULT_LANG } from "../content/config";
|
||||
export { type Lang, DEFAULT_LANG } from "../content/config";
|
||||
|
||||
const UI_STRINGS = {
|
||||
// Utility functions
|
||||
|
@ -106,6 +105,14 @@ const UI_STRINGS = {
|
|||
en: "Copyright",
|
||||
tok: "toki lawa",
|
||||
},
|
||||
"published_content/summary": {
|
||||
en: "Summary",
|
||||
tok: "lipu tawa tenpo lili",
|
||||
},
|
||||
"published_content/reveal_summary": {
|
||||
en: "Click to reveal",
|
||||
tok: "Click to reveal summary in English",
|
||||
},
|
||||
// Story page-specific strings
|
||||
"story/return_to_stories": {
|
||||
en: "Return to stories",
|
||||
|
@ -134,14 +141,6 @@ const UI_STRINGS = {
|
|||
en: "Story",
|
||||
tok: "lipu",
|
||||
},
|
||||
"story/summary": {
|
||||
en: "Summary",
|
||||
tok: "lipu tawa tenpo lili",
|
||||
},
|
||||
"story/reveal_summary": {
|
||||
en: "Click to reveal",
|
||||
tok: "Click to reveal summary in English",
|
||||
},
|
||||
"story/previous_story": {
|
||||
en: (title: string) => `Previous: ${title}`,
|
||||
},
|
||||
|
@ -218,6 +217,18 @@ const UI_STRINGS = {
|
|||
en: (platforms: GamePlatform[], contentWarning: string) =>
|
||||
platforms.length > 0 ? `${UI_STRINGS["game/platforms"].en(platforms)} ${contentWarning}` : contentWarning,
|
||||
},
|
||||
"game/previous_game": {
|
||||
en: (title: string) => `Previous: ${title}`,
|
||||
},
|
||||
"game/previous_game_aria_label": {
|
||||
en: "Previous game",
|
||||
},
|
||||
"game/next_game": {
|
||||
en: (title: string) => `Next: ${title}`,
|
||||
},
|
||||
"game/next_game_aria_label": {
|
||||
en: "Next game",
|
||||
},
|
||||
"game/article_aria_label": {
|
||||
en: "Game",
|
||||
},
|
||||
|
|
|
@ -47,7 +47,7 @@ const copyrightYear = currentYear > FIRST_YEAR ? `${FIRST_YEAR}–${currentYear}
|
|||
</div>
|
||||
<div class="mt-2 flex items-center gap-x-1 pb-10">
|
||||
<a class="text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelled-by="label-main-website">
|
||||
<svg width="1.5rem" height="1.5rem" viewBox="0 0 576 512" class="fill-current" aria-hidden>
|
||||
<svg style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} viewBox="0 0 576 512" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
|
@ -55,7 +55,7 @@ const copyrightYear = currentYear > FIRST_YEAR ? `${FIRST_YEAR}–${currentYear}
|
|||
<span id="label-main-website" class="hidden">Main website</span>
|
||||
</a>
|
||||
<a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
|
||||
<svg width="1.5rem" height="1.5rem" viewBox="0 0 448 512" class="fill-current" aria-hidden>
|
||||
<svg style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} viewBox="0 0 448 512" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
|
@ -63,12 +63,22 @@ const copyrightYear = currentYear > FIRST_YEAR ? `${FIRST_YEAR}–${currentYear}
|
|||
<span id="label-rss-feed" class="hidden">RSS feed</span>
|
||||
</a>
|
||||
<button data-dark-mode class="text-link hidden p-1" aria-labelledby="label-toggle-dark-mode" aria-hidden>
|
||||
<svg width="1.5rem" height="1.5rem" viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden>
|
||||
<svg
|
||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
||||
viewBox="0 0 512 512"
|
||||
class="hidden dark:block"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg width="1.5rem" height="1.5rem" viewBox="0 0 512 512" class="block fill-current dark:hidden" aria-hidden>
|
||||
<svg
|
||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
||||
viewBox="0 0 512 512"
|
||||
class="block dark:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
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>
|
||||
|
|
|
@ -1,303 +1,71 @@
|
|||
---
|
||||
import { getImage } from "astro:assets";
|
||||
import { type CollectionEntry, getEntry, getEntries, getCollection } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import { DEFAULT_LANG, t } from "../i18n";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import { type CollectionEntry, getEntry, getEntries } from "astro:content";
|
||||
import PublishedContentLayout from "./PublishedContentLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
import Authors from "../components/Authors.astro";
|
||||
import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
|
||||
import Prose from "../components/Prose.astro";
|
||||
import MastodonComments from "../components/MastodonComments.astro";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
|
||||
type Props = CollectionEntry<"games">["data"];
|
||||
|
||||
const { props } = Astro;
|
||||
const prev = props.prev && (await getEntry(props.prev));
|
||||
const next = props.next && (await getEntry(props.next));
|
||||
const series = props.series && (await getEntry(props.series));
|
||||
const authorsList = await getEntries(props.authors);
|
||||
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const categorizedTags = Object.fromEntries(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||
typeof name === "string" ? [name, name] : [name[DEFAULT_LANG], name[props.lang] ?? null],
|
||||
),
|
||||
),
|
||||
);
|
||||
const tags = props.tags.map<{ id: string; name: string }>((tag) => {
|
||||
const id = slug(tag);
|
||||
if (!(tag in categorizedTags)) {
|
||||
console.warn(`Tag "${tag}" doesn't have a category in the "tag-categories" collection`);
|
||||
return { id, name: tag };
|
||||
}
|
||||
if (categorizedTags[tag] == null) {
|
||||
console.warn(`No "${props.lang}" translation for tag "${tag}"`);
|
||||
return { id, name: tag };
|
||||
}
|
||||
return { id, name: categorizedTags[tag] };
|
||||
});
|
||||
const thumbnail =
|
||||
props.thumbnail &&
|
||||
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||
<meta property="og:description" content={t(props.lang, "game/warnings", props.platforms, props.contentWarning)} />
|
||||
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
||||
{
|
||||
thumbnail ? (
|
||||
<Fragment>
|
||||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
data-pagefind-meta="image_alt[content]"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
<meta name="theme-color" content="#7DD05A" data-react-helmet="true" />
|
||||
<PublishedContentLayout
|
||||
publishedContentType="game"
|
||||
title={props.title}
|
||||
lang={props.lang}
|
||||
isDraft={props.isDraft}
|
||||
pubDate={props.pubDate}
|
||||
description={props.description}
|
||||
summary={undefined}
|
||||
tags={props.tags}
|
||||
thumbnail={props.thumbnail}
|
||||
thumbnailWidth={props.thumbnailWidth}
|
||||
thumbnailHeight={props.thumbnailHeight}
|
||||
copyrightedCharacters={props.copyrightedCharacters}
|
||||
series={series}
|
||||
prev={prev && !prev.data.isDraft
|
||||
? { link: `/games/${prev.slug}`, title: t(props.lang, "game/previous_game", prev.data.title) }
|
||||
: undefined}
|
||||
next={next && !next.data.isDraft
|
||||
? { link: `/games/${next.slug}`, title: t(props.lang, "game/next_game", next.data.title) }
|
||||
: undefined}
|
||||
relatedStories={relatedStories}
|
||||
relatedGames={relatedGames}
|
||||
posts={props.posts}
|
||||
labelReturnTo={{ title: t(props.lang, "game/return_to_games"), link: "/games" }}
|
||||
labelPreviousContent={t(props.lang, "game/previous_game_aria_label")}
|
||||
labelNextContent={t(props.lang, "game/next_game_aria_label")}
|
||||
labelTitleSection={t(props.lang, "game/title_aria_label")}
|
||||
labelInformationSection={t(props.lang, "game/information_aria_label")}
|
||||
labelArticleSection={t(props.lang, "game/article_aria_label")}
|
||||
>
|
||||
<meta
|
||||
slot="head-description"
|
||||
property="og:description"
|
||||
content={t(props.lang, "game/warnings", props.platforms, props.contentWarning)}
|
||||
/>
|
||||
<Fragment slot="section-information">
|
||||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||
</Authors>
|
||||
<div id="platforms">
|
||||
<p>{t(props.lang, "game/platforms", props.platforms)}</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
<div
|
||||
id="top"
|
||||
class="min-w-screen relative min-h-screen bg-radial from-bm-300 to-bm-600 px-1 pb-16 pt-20 dark:from-green-700 dark:to-green-950 print:bg-none print:pb-0 print:pt-0"
|
||||
>
|
||||
<div
|
||||
id="toolbox-buttons"
|
||||
aria-label="Toolbox"
|
||||
class="pointer-events-none absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
||||
>
|
||||
<a href={series ? series.data.url : "/games"} class="text-link my-1 p-2" aria-labelled-by="label-return-to">
|
||||
<svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-return-to">
|
||||
{
|
||||
series
|
||||
? t(props.lang, "published_content/return_to_series", series.data.name)
|
||||
: t(props.lang, "game/return_to_games")
|
||||
}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="#description"
|
||||
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-labelled-by="label-go-to-description"
|
||||
>
|
||||
<svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-go-to-description">{t(props.lang, "published_content/go_to_description")}</span
|
||||
>
|
||||
</a>
|
||||
<button
|
||||
data-dark-mode
|
||||
class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-labelled-by="label-toggle-dark-mode"
|
||||
aria-hidden
|
||||
>
|
||||
<svg
|
||||
width="1.25rem"
|
||||
height="1.25rem"
|
||||
viewBox="0 0 512 512"
|
||||
class="hidden fill-current dark:block"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
width="1.25rem"
|
||||
height="1.25rem"
|
||||
viewBox="0 0 512 512"
|
||||
class="block fill-current dark:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<main
|
||||
class="mx-auto max-w-3xl rounded-lg bg-stone-50 px-2 pb-4 pt-1 shadow-sm dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none"
|
||||
data-pagefind-body={props.isDraft ? undefined : ""}
|
||||
data-pagefind-meta="type:game"
|
||||
>
|
||||
<h1
|
||||
id="game-title"
|
||||
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
aria-label={t(props.lang, "game/title_aria_label")}
|
||||
>
|
||||
{props.title}
|
||||
</h1>
|
||||
<section
|
||||
id="game-information"
|
||||
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
|
||||
aria-label={t(props.lang, "game/information_aria_label")}
|
||||
>
|
||||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||
</Authors>
|
||||
<div id="platforms">
|
||||
<p>{t(props.lang, "game/platforms", props.platforms)}</p>
|
||||
</div>
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
props.contentWarning ? (
|
||||
<div id="content-warning">
|
||||
<p>{props.contentWarning}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</section>
|
||||
{
|
||||
thumbnail ? (
|
||||
<Fragment>
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<img
|
||||
loading="eager"
|
||||
src={thumbnail.src}
|
||||
alt={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
data-pagefind-meta="image[src],image_alt[alt]"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<article id="game" class="pr-1 font-serif" aria-label={t(props.lang, "game/article_aria_label")}>
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
</article>
|
||||
<hr class="mx-auto mb-6 mt-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p
|
||||
id="draft-warning-bottom"
|
||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||
>
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||
>
|
||||
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
<div class="pr-3 text-right print:hidden">
|
||||
<a href="#top" class="text-link inline-flex items-center underline"
|
||||
><svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 384 512" aria-hidden
|
||||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
relatedStories.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedStories.map((story) => (
|
||||
<li>
|
||||
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
relatedGames.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_games")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedGames.map((game) => (
|
||||
<li>
|
||||
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
tags.length > 0 ? (
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white print:bg-none">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${id}`}>
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<div
|
||||
class="pt-6 text-center text-xs text-black dark:text-white"
|
||||
aria-label={t(props.lang, "published_content/copyright_aria_label")}
|
||||
>
|
||||
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
<div slot="section-content-warning" id="content-warning">
|
||||
<p>{props.contentWarning}</p>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<Fragment slot="section-article">
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
</Fragment>
|
||||
</PublishedContentLayout>
|
||||
|
|
444
src/layouts/PublishedContentLayout.astro
Normal file
444
src/layouts/PublishedContentLayout.astro
Normal file
|
@ -0,0 +1,444 @@
|
|||
---
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { getImage } from "astro:assets";
|
||||
import { type CollectionEntry, getEntry, getCollection } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import { DEFAULT_LANG, t, type Lang } from "../i18n";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
|
||||
import Prose from "../components/Prose.astro";
|
||||
import MastodonComments from "../components/MastodonComments.astro";
|
||||
import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
|
||||
|
||||
interface RelatedContent {
|
||||
link: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
/* Content attributes */
|
||||
title: string;
|
||||
lang: Lang;
|
||||
isDraft: boolean;
|
||||
pubDate?: Date;
|
||||
description: string;
|
||||
summary?: string;
|
||||
tags: string[];
|
||||
thumbnail?: ImageMetadata;
|
||||
thumbnailWidth?: number;
|
||||
thumbnailHeight?: number;
|
||||
copyrightedCharacters?: CopyrightedCharactersType;
|
||||
series?: CollectionEntry<"series">;
|
||||
prev?: RelatedContent;
|
||||
next?: RelatedContent;
|
||||
relatedStories?: CollectionEntry<"stories">[];
|
||||
relatedGames?: CollectionEntry<"games">[];
|
||||
posts: {
|
||||
mastodon?: {
|
||||
link: string;
|
||||
instance: string;
|
||||
user: string;
|
||||
postId: string;
|
||||
};
|
||||
};
|
||||
|
||||
/* Layout attributes */
|
||||
publishedContentType: "story" | "game";
|
||||
labelReturnTo: RelatedContent;
|
||||
labelPreviousContent: string;
|
||||
labelNextContent: string;
|
||||
labelTitleSection: string;
|
||||
labelInformationSection: string;
|
||||
labelArticleSection: string;
|
||||
};
|
||||
|
||||
const { props } = Astro;
|
||||
const series = props.series && (await getEntry(props.series));
|
||||
const categorizedTags = Object.fromEntries(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||
typeof name === "string" ? [name, name] : [name[DEFAULT_LANG], name[props.lang] ?? null],
|
||||
),
|
||||
),
|
||||
);
|
||||
const tags = props.tags.map<{ id: string; name: string }>((tag) => {
|
||||
const tagSlug = slug(tag);
|
||||
if (!(tag in categorizedTags)) {
|
||||
console.warn(`Tag "${tag}" doesn't have a category in the "tag-categories" collection`);
|
||||
return { id: tagSlug, name: tag };
|
||||
}
|
||||
if (categorizedTags[tag] == null) {
|
||||
console.warn(`No "${props.lang}" translation for tag "${tag}"`);
|
||||
return { id: tagSlug, name: tag };
|
||||
}
|
||||
return { id: tagSlug, name: categorizedTags[tag] };
|
||||
});
|
||||
const thumbnail =
|
||||
props.thumbnail &&
|
||||
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||
<slot name="head-description" />
|
||||
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
||||
{
|
||||
thumbnail ? (
|
||||
<Fragment>
|
||||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
data-pagefind-meta="image_alt[content]"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
<meta name="theme-color" content="#7DD05A" data-react-helmet="true" />
|
||||
</Fragment>
|
||||
<div
|
||||
id="top"
|
||||
class="min-w-screen relative min-h-screen bg-radial from-bm-300 to-bm-600 px-1 pb-16 pt-20 dark:from-green-700 dark:to-green-950 print:bg-none print:pb-0 print:pt-0"
|
||||
>
|
||||
<div
|
||||
id="toolbox-buttons"
|
||||
aria-label="Toolbox"
|
||||
class="pointer-events-none absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
||||
>
|
||||
<a
|
||||
href={series ? series.data.url : props.labelReturnTo.link}
|
||||
class="text-link my-1 p-2"
|
||||
aria-labelled-by="label-return-to"
|
||||
>
|
||||
<svg style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} viewBox="0 0 512 512" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-return-to"
|
||||
>{
|
||||
series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title
|
||||
}</span
|
||||
>
|
||||
</a>
|
||||
<a
|
||||
href="#description"
|
||||
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-labelled-by="label-go-to-description"
|
||||
>
|
||||
<svg style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} viewBox="0 0 512 512" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-go-to-description">{t(props.lang, "published_content/go_to_description")}</span
|
||||
>
|
||||
</a>
|
||||
<button
|
||||
data-dark-mode
|
||||
class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-labelled-by="label-toggle-dark-mode"
|
||||
aria-hidden
|
||||
>
|
||||
<svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
viewBox="0 0 512 512"
|
||||
class="hidden dark:block"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
viewBox="0 0 512 512"
|
||||
class="block dark:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<main
|
||||
class="mx-auto max-w-3xl rounded-lg bg-stone-50 px-2 pb-4 pt-1 shadow-sm dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none"
|
||||
data-pagefind-body={props.isDraft ? undefined : ""}
|
||||
data-pagefind-meta={`type:${props.publishedContentType}`}
|
||||
>
|
||||
{
|
||||
props.prev || props.next ? (
|
||||
<div class="print:hidden">
|
||||
<div id="nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||
{props.prev ? (
|
||||
<a
|
||||
href={props.prev.link}
|
||||
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
||||
aria-label={props.labelPreviousContent}
|
||||
>
|
||||
<svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
class="mr-1"
|
||||
viewBox="0 0 320 512"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
||||
</svg>
|
||||
<span>{props.prev.title}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="h-full border-r border-stone-400 dark:border-stone-600" aria-hidden />
|
||||
)}
|
||||
{props.next ? (
|
||||
<a
|
||||
href={props.next.link}
|
||||
class="text-link flex items-center justify-center px-1 py-3 font-light underline"
|
||||
aria-label={props.labelNextContent}
|
||||
>
|
||||
<span>{props.next.title}</span>
|
||||
<svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
class="ml-1"
|
||||
viewBox="0 0 320 512"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<div aria-hidden />
|
||||
)}
|
||||
</div>
|
||||
<hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<h1
|
||||
id="section-title"
|
||||
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
aria-label={props.labelTitleSection}
|
||||
>
|
||||
{props.title}
|
||||
</h1>
|
||||
<section
|
||||
id="section-information"
|
||||
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
|
||||
aria-label={props.labelInformationSection}
|
||||
>
|
||||
<slot name="section-information" />
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<slot name="section-content-warning" />
|
||||
</section>
|
||||
{
|
||||
thumbnail ? (
|
||||
<Fragment>
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<img
|
||||
loading="eager"
|
||||
src={thumbnail.src}
|
||||
alt={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<article id="content" class="pr-1 font-serif" aria-label={props.labelArticleSection}>
|
||||
<slot name="section-article" />
|
||||
</article>
|
||||
<hr class="mx-auto mb-6 mt-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p
|
||||
id="draft-warning-bottom"
|
||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||
>
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||
>
|
||||
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
{
|
||||
props.summary ? (
|
||||
<section id="summary" class="px-2 font-serif" aria-describedby="title-summary">
|
||||
<h2 id="title-summary" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/summary")}
|
||||
</h2>
|
||||
<details class="mb-6 mt-1 rounded-lg border border-stone-400 bg-stone-50 text-stone-800 dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100">
|
||||
<summary class="rounded-lg bg-stone-200 px-2 py-1 dark:bg-stone-800">
|
||||
{t(props.lang, "published_content/reveal_summary")}
|
||||
</summary>
|
||||
<div class="px-2 py-1">
|
||||
<Prose>
|
||||
<Markdown of={props.summary} />
|
||||
</Prose>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
<div class="pr-3 text-right print:hidden">
|
||||
<a href="#top" class="text-link inline-flex items-center underline" aria-labelledby="label-to-top"
|
||||
><svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
class="mr-1"
|
||||
viewBox="0 0 384 512"
|
||||
aria-hidden
|
||||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span id="label-to-top">{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
props.prev || props.next ? (
|
||||
<Fragment>
|
||||
<hr class="mx-auto mt-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
|
||||
<div id="nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||
{props.prev ? (
|
||||
<a
|
||||
href={props.prev.link}
|
||||
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
||||
aria-label={props.labelPreviousContent}
|
||||
>
|
||||
<svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
class="mr-1"
|
||||
viewBox="0 0 320 512"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
||||
</svg>
|
||||
<span>{props.prev.title}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="h-full border-r border-stone-400 dark:border-stone-600" />
|
||||
)}
|
||||
{props.next ? (
|
||||
<a
|
||||
href={props.next.link}
|
||||
class="text-link flex items-center justify-center px-1 py-3 font-light underline"
|
||||
aria-label={props.labelNextContent}
|
||||
>
|
||||
<span>{props.next.title}</span>
|
||||
<svg
|
||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
||||
class="ml-1"
|
||||
viewBox="0 0 320 512"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
<hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
props.relatedStories?.length ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{props.relatedStories.map((story) => (
|
||||
<li>
|
||||
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
props.relatedGames?.length ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_games")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{props.relatedGames.map((game) => (
|
||||
<li>
|
||||
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
tags.length ? (
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white print:bg-none">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${id}`}>
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<div
|
||||
class="pt-6 text-center text-xs text-black dark:text-white"
|
||||
aria-label={t(props.lang, "published_content/copyright_aria_label")}
|
||||
>
|
||||
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
|
@ -1,17 +1,12 @@
|
|||
---
|
||||
import { getImage } from "astro:assets";
|
||||
import { type CollectionEntry, getEntry, getEntries, getCollection } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import { DEFAULT_LANG, t } from "../i18n";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import { type CollectionEntry, getEntry, getEntries } from "astro:content";
|
||||
import PublishedContentLayout from "./PublishedContentLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
import Authors from "../components/Authors.astro";
|
||||
import Commissioners from "../components/Commissioners.astro";
|
||||
import Requesters from "../components/Requesters.astro";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
|
||||
import Prose from "../components/Prose.astro";
|
||||
import MastodonComments from "../components/MastodonComments.astro";
|
||||
|
||||
type Props = CollectionEntry<"stories">["data"];
|
||||
|
||||
|
@ -24,393 +19,81 @@ const commissionersList = props.commissioner && (await getEntries(props.commissi
|
|||
const requestersList = props.requester && (await getEntries(props.requester));
|
||||
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const categorizedTags = Object.fromEntries(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||
typeof name === "string" ? [name, name] : [name[DEFAULT_LANG], name[props.lang] ?? null],
|
||||
),
|
||||
),
|
||||
);
|
||||
const tags = props.tags.map<{ id: string; name: string }>((tag) => {
|
||||
const tagSlug = slug(tag);
|
||||
if (!(tag in categorizedTags)) {
|
||||
console.warn(`Tag "${tag}" doesn't have a category in the "tag-categories" collection`);
|
||||
return { id: tagSlug, name: tag };
|
||||
}
|
||||
if (categorizedTags[tag] == null) {
|
||||
console.warn(`No "${props.lang}" translation for tag "${tag}"`);
|
||||
return { id: tagSlug, name: tag };
|
||||
}
|
||||
return { id: tagSlug, name: categorizedTags[tag] };
|
||||
});
|
||||
const thumbnail =
|
||||
props.thumbnail &&
|
||||
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
||||
const wordCount = props.wordCount?.toString();
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||
<meta property="og:description" content={t(props.lang, "story/warnings", wordCount, props.contentWarning)} />
|
||||
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
||||
<PublishedContentLayout
|
||||
publishedContentType="story"
|
||||
title={props.title}
|
||||
lang={props.lang}
|
||||
isDraft={props.isDraft}
|
||||
pubDate={props.pubDate}
|
||||
description={props.description}
|
||||
summary={props.summary}
|
||||
tags={props.tags}
|
||||
thumbnail={props.thumbnail}
|
||||
thumbnailWidth={props.thumbnailWidth}
|
||||
thumbnailHeight={props.thumbnailHeight}
|
||||
copyrightedCharacters={props.copyrightedCharacters}
|
||||
series={series}
|
||||
prev={prev && !prev.data.isDraft
|
||||
? {
|
||||
link: `/stories/${prev.slug}`,
|
||||
title: t(props.lang, "story/previous_story", prev.data.shortTitle || prev.data.title),
|
||||
}
|
||||
: undefined}
|
||||
next={next && !next.data.isDraft
|
||||
? {
|
||||
link: `/stories/${next.slug}`,
|
||||
title: t(props.lang, "story/next_story", next.data.shortTitle || next.data.title),
|
||||
}
|
||||
: undefined}
|
||||
relatedStories={relatedStories}
|
||||
relatedGames={relatedGames}
|
||||
posts={props.posts}
|
||||
labelReturnTo={{ title: t(props.lang, "story/return_to_stories"), link: "/stories/1" }}
|
||||
labelPreviousContent={t(props.lang, "story/previous_story_aria_label")}
|
||||
labelNextContent={t(props.lang, "story/next_story_aria_label")}
|
||||
labelTitleSection={t(props.lang, "story/title_aria_label")}
|
||||
labelInformationSection={t(props.lang, "story/information_aria_label")}
|
||||
labelArticleSection={t(props.lang, "story/article_aria_label")}
|
||||
>
|
||||
<meta
|
||||
slot="head-description"
|
||||
property="og:description"
|
||||
content={t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
||||
/>
|
||||
<Fragment slot="section-information">
|
||||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||
</Authors>
|
||||
{
|
||||
thumbnail ? (
|
||||
<Fragment>
|
||||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
|
||||
data-pagefind-meta="image_alt[content]"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
requestersList && (
|
||||
<Requesters lang={props.lang}>
|
||||
{requestersList.map((requester) => (
|
||||
<UserComponent user={requester} lang={props.lang} />
|
||||
))}
|
||||
</Requesters>
|
||||
)
|
||||
}
|
||||
{
|
||||
commissionersList && (
|
||||
<Commissioners lang={props.lang}>
|
||||
{commissionersList.map((commissioner) => (
|
||||
<UserComponent user={commissioner} lang={props.lang} />
|
||||
))}
|
||||
</Commissioners>
|
||||
)
|
||||
}
|
||||
<meta name="theme-color" content="#7DD05A" data-react-helmet="true" />
|
||||
</Fragment>
|
||||
<div
|
||||
id="top"
|
||||
class="min-w-screen relative min-h-screen bg-radial from-bm-300 to-bm-600 px-1 pb-16 pt-20 dark:from-green-700 dark:to-green-950 print:bg-none print:pb-0 print:pt-0"
|
||||
>
|
||||
<div
|
||||
id="toolbox-buttons"
|
||||
aria-label="Toolbox"
|
||||
class="pointer-events-none absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
||||
>
|
||||
<a href={series ? series.data.url : "/stories/1"} class="text-link my-1 p-2" aria-labelled-by="label-return-to">
|
||||
<svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-return-to"
|
||||
>{
|
||||
series
|
||||
? t(props.lang, "published_content/return_to_series", series.data.name)
|
||||
: t(props.lang, "story/return_to_stories")
|
||||
}</span
|
||||
>
|
||||
</a>
|
||||
<a
|
||||
href="#description"
|
||||
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-labelled-by="label-go-to-description"
|
||||
>
|
||||
<svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-go-to-description">{t(props.lang, "published_content/go_to_description")}</span
|
||||
>
|
||||
</a>
|
||||
<button
|
||||
data-dark-mode
|
||||
class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-labelled-by="label-toggle-dark-mode"
|
||||
aria-hidden
|
||||
>
|
||||
<svg
|
||||
width="1.25rem"
|
||||
height="1.25rem"
|
||||
viewBox="0 0 512 512"
|
||||
class="hidden fill-current dark:block"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
width="1.25rem"
|
||||
height="1.25rem"
|
||||
viewBox="0 0 512 512"
|
||||
class="block fill-current dark:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
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>
|
||||
</svg>
|
||||
<span class="hidden" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<main
|
||||
class="mx-auto max-w-3xl rounded-lg bg-stone-50 px-2 pb-4 pt-1 shadow-sm dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none"
|
||||
data-pagefind-body={props.isDraft ? undefined : ""}
|
||||
data-pagefind-meta="type:story"
|
||||
>
|
||||
{
|
||||
(prev && !prev.data.isDraft) || (next && !next.data.isDraft) ? (
|
||||
<div class="print:hidden">
|
||||
<div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||
{prev && !prev.data.isDraft ? (
|
||||
<a
|
||||
href={`/stories/${prev.slug}`}
|
||||
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
||||
aria-label={t(props.lang, "story/previous_story_aria_label")}
|
||||
>
|
||||
<svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 320 512" aria-hidden>
|
||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
||||
</svg>
|
||||
<span>{t(props.lang, "story/previous_story", prev.data.shortTitle || prev.data.title)}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="h-full border-r border-stone-400 dark:border-stone-600" aria-hidden />
|
||||
)}
|
||||
{next && !next.data.isDraft ? (
|
||||
<a
|
||||
href={`/stories/${next.slug}`}
|
||||
class="text-link flex items-center justify-center px-1 py-3 font-light underline"
|
||||
aria-label={t(props.lang, "story/next_story_aria_label")}
|
||||
>
|
||||
<span>{t(props.lang, "story/next_story", next.data.shortTitle || next.data.title)}</span>
|
||||
<svg width="1.25rem" height="1.25rem" class="ml-1 fill-current" viewBox="0 0 320 512" aria-hidden>
|
||||
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<div aria-hidden />
|
||||
)}
|
||||
</div>
|
||||
<hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<h1
|
||||
id="story-title"
|
||||
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
aria-label={t(props.lang, "story/title_aria_label")}
|
||||
>
|
||||
{props.title}
|
||||
</h1>
|
||||
<section
|
||||
id="story-information"
|
||||
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
|
||||
aria-label={t(props.lang, "story/information_aria_label")}
|
||||
>
|
||||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||
</Authors>
|
||||
{
|
||||
requestersList && (
|
||||
<Requesters lang={props.lang}>
|
||||
{requestersList.map((requester) => (
|
||||
<UserComponent user={requester} lang={props.lang} />
|
||||
))}
|
||||
</Requesters>
|
||||
)
|
||||
}
|
||||
{
|
||||
commissionersList && (
|
||||
<Commissioners lang={props.lang}>
|
||||
{commissionersList.map((commissioner) => (
|
||||
<UserComponent user={commissioner} lang={props.lang} />
|
||||
))}
|
||||
</Commissioners>
|
||||
)
|
||||
}
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<div id="content-warning">
|
||||
<p>
|
||||
{t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{
|
||||
thumbnail ? (
|
||||
<Fragment>
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<img
|
||||
loading="eager"
|
||||
src={thumbnail.src}
|
||||
alt={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
<article id="story" class="pr-1 font-serif" aria-label={t(props.lang, "story/article_aria_label")}>
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
</article>
|
||||
<hr class="mx-auto mb-6 mt-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p
|
||||
id="draft-warning-bottom"
|
||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||
>
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||
>
|
||||
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
{
|
||||
props.summary ? (
|
||||
<section id="summary" class="px-2 font-serif" aria-describedby="title-summary">
|
||||
<h2 id="title-summary" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "story/summary")}
|
||||
</h2>
|
||||
<details class="mb-6 mt-1 rounded-lg border border-stone-400 bg-stone-50 text-stone-800 dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100">
|
||||
<summary class="rounded-lg bg-stone-200 px-2 py-1 dark:bg-stone-800">
|
||||
{t(props.lang, "story/reveal_summary")}
|
||||
</summary>
|
||||
<div class="px-2 py-1">
|
||||
<Prose>
|
||||
<Markdown of={props.summary} />
|
||||
</Prose>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
<div class="pr-3 text-right print:hidden">
|
||||
<a href="#top" class="text-link inline-flex items-center underline" aria-labelledby="label-to-top"
|
||||
><svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 384 512" aria-hidden
|
||||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span id="label-to-top">{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
prev || next ? (
|
||||
<Fragment>
|
||||
<hr class="mx-auto mt-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
|
||||
<div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||
{prev ? (
|
||||
<a
|
||||
href={`/stories/${prev.slug}`}
|
||||
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
||||
aria-label={t(props.lang, "story/previous_story_aria_label")}
|
||||
>
|
||||
<svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 320 512" aria-hidden>
|
||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
||||
</svg>
|
||||
<span>{t(props.lang, "story/previous_story", prev.data.shortTitle || prev.data.title)}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="h-full border-r border-stone-400 dark:border-stone-600" />
|
||||
)}
|
||||
{next ? (
|
||||
<a
|
||||
href={`/stories/${next.slug}`}
|
||||
class="text-link flex items-center justify-center px-1 py-3 font-light underline"
|
||||
aria-label={t(props.lang, "story/next_story_aria_label")}
|
||||
>
|
||||
<span>{t(props.lang, "story/next_story", next.data.shortTitle || next.data.title)}</span>
|
||||
<svg width="1.25rem" height="1.25rem" class="ml-1 fill-current" viewBox="0 0 320 512" aria-hidden>
|
||||
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
<hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
relatedStories.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedStories.map((story) => (
|
||||
<li>
|
||||
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
relatedGames.length > 0 ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/related_games")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedGames.map((game) => (
|
||||
<li>
|
||||
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
tags.length > 0 ? (
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white print:bg-none">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${id}`}>
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<div
|
||||
class="pt-6 text-center text-xs text-black dark:text-white"
|
||||
aria-label={t(props.lang, "published_content/copyright_aria_label")}
|
||||
>
|
||||
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
<div slot="section-content-warning" id="content-warning">
|
||||
<p>
|
||||
{t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
||||
</p>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<Fragment slot="section-article">
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
</Fragment>
|
||||
</PublishedContentLayout>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { APIRoute, GetStaticPaths } from "astro";
|
||||
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
|
||||
import type { Lang, Website } from "../../../content/config";
|
||||
import { t } from "../../../i18n";
|
||||
import type { Website } from "../../../content/config";
|
||||
import { t, type Lang } from "../../../i18n";
|
||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
||||
import { getUsernameForLang } from "../../../utils/get_username_for_lang";
|
||||
|
|
|
@ -3,8 +3,7 @@ import type { APIRoute } from "astro";
|
|||
import { getCollection, getEntries, type CollectionEntry, type CollectionKey } from "astro:content";
|
||||
import { markdown } from "@astropub/md";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { t } from "../i18n";
|
||||
import type { Lang } from "../content/config";
|
||||
import { t, type Lang } from "../i18n";
|
||||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
---
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content";
|
||||
import { Image } from "astro:assets";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { CollectionEntry } from "astro:content";
|
||||
import { DEFAULT_LANG, type Lang } from "../content/config";
|
||||
import { DEFAULT_LANG, type Lang } from "../i18n";
|
||||
|
||||
export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang): string {
|
||||
const { name } = user.data;
|
||||
|
|
Loading…
Add table
Reference in a new issue