diff --git a/package-lock.json b/package-lock.json index 1a3ee93..8dafd67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0a8020c..4519d7e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/AgeRestrictedModal.astro b/src/components/AgeRestrictedModal.astro index 4db69fd..6d85e6b 100644 --- a/src/components/AgeRestrictedModal.astro +++ b/src/components/AgeRestrictedModal.astro @@ -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> diff --git a/src/components/Authors.astro b/src/components/Authors.astro index 89a156c..c4ddae3 100644 --- a/src/components/Authors.astro +++ b/src/components/Authors.astro @@ -1,6 +1,5 @@ --- -import type { Lang } from "../content/config"; -import { t } from "../i18n"; +import { t, type Lang } from "../i18n"; type Props = { lang: Lang; diff --git a/src/components/Commissioners.astro b/src/components/Commissioners.astro index cb3f5f7..4c34b2b 100644 --- a/src/components/Commissioners.astro +++ b/src/components/Commissioners.astro @@ -1,6 +1,5 @@ --- -import type { Lang } from "../content/config"; -import { t } from "../i18n"; +import { t, type Lang } from "../i18n"; type Props = { lang: Lang; diff --git a/src/components/CopyrightedCharacters.astro b/src/components/CopyrightedCharacters.astro index 156ac5e..6de2ad6 100644 --- a/src/components/CopyrightedCharacters.astro +++ b/src/components/CopyrightedCharacters.astro @@ -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"; diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro index 5221167..790adf6 100644 --- a/src/components/MastodonComments.astro +++ b/src/components/MastodonComments.astro @@ -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"); diff --git a/src/components/Requesters.astro b/src/components/Requesters.astro index 245af9b..9c4f8e2 100644 --- a/src/components/Requesters.astro +++ b/src/components/Requesters.astro @@ -1,6 +1,5 @@ --- -import type { Lang } from "../content/config"; -import { t } from "../i18n"; +import { t, type Lang } from "../i18n"; type Props = { lang: Lang; diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro index 8582f6b..c82c8b4 100644 --- a/src/components/UserComponent.astro +++ b/src/components/UserComponent.astro @@ -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 = { diff --git a/src/content/config.ts b/src/content/config.ts index a912c2b..70089cf 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -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`) diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 62f6951..ef21b7a 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -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", }, diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro index 4aa945a..7317d32 100644 --- a/src/layouts/GalleryLayout.astro +++ b/src/layouts/GalleryLayout.astro @@ -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> diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro index 7cd5330..800ed77 100644 --- a/src/layouts/GameLayout.astro +++ b/src/layouts/GameLayout.astro @@ -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> diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro new file mode 100644 index 0000000..2571500 --- /dev/null +++ b/src/layouts/PublishedContentLayout.astro @@ -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> diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro index 192b729..6b775c0 100644 --- a/src/layouts/StoryLayout.astro +++ b/src/layouts/StoryLayout.astro @@ -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> diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts index 4ea70e8..ac57980 100644 --- a/src/pages/api/export-story/[...slug].ts +++ b/src/pages/api/export-story/[...slug].ts @@ -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"; diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index 57785f3..24165db 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -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"; diff --git a/src/pages/index.astro b/src/pages/index.astro index e8609e4..8bb66e4 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -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"; diff --git a/src/utils/get_username_for_lang.ts b/src/utils/get_username_for_lang.ts index f2cace8..eef4f74 100644 --- a/src/utils/get_username_for_lang.ts +++ b/src/utils/get_username_for_lang.ts @@ -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;