Fix up first version and add Prettier and Docker
This commit is contained in:
parent
09a1919d36
commit
324050ee38
87 changed files with 2210 additions and 822 deletions
|
|
@ -4,19 +4,33 @@ import { getCollection } from "astro:content";
|
|||
import { getUnixTime } from "date-fns";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
|
||||
const stories = (await getCollection('stories')).filter(story => !story.data.isDraft).sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate))
|
||||
const stories = (await getCollection("stories"))
|
||||
.filter((story) => !story.data.isDraft)
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate));
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Stories">
|
||||
<h1>Stories</h1>
|
||||
<p>Lorem ipsum.</p>
|
||||
<ul>
|
||||
{stories.map((story) =>
|
||||
<li>
|
||||
<a href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && <Image src={story.data.thumbnail} alt={`Thumbnail for ${story.data.title}`} width={story.data.thumbnailWidth} height={story.data.thumbnailHeight} />}
|
||||
<span>{story.data.pubDate} - {story.data.title}</span>
|
||||
</a>
|
||||
</li>)}
|
||||
{
|
||||
stories.map((story) => (
|
||||
<li>
|
||||
<a href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
<Image
|
||||
src={story.data.thumbnail}
|
||||
alt={`Thumbnail for ${story.data.title}`}
|
||||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{story.data.pubDate} - {story.data.title}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle="Age verification">
|
||||
<div
|
||||
class="bg-stone-100 dark:bg-stone-900"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight"
|
||||
>
|
||||
<div class="bg-stone-100 dark:bg-stone-900">
|
||||
<div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
|
||||
<div class="h-14 w-14 text-bm-500 sm:h-16 sm:w-16 dark:text-bm-400">
|
||||
<svg class="fill-current" 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
|
||||
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>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="pb-3 pt-2 text-2xl font-light text-stone-700 sm:pb-4 sm:pt-2 sm:text-3xl dark:text-stone-50"
|
||||
>
|
||||
<div class="pb-3 pt-2 text-2xl font-light text-stone-700 sm:pb-4 sm:pt-2 sm:text-3xl dark:text-stone-50">
|
||||
Age verification
|
||||
</div>
|
||||
<div class="w-full max-w-xl">
|
||||
|
|
@ -27,12 +23,10 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
|||
</div>
|
||||
</div>
|
||||
<p class="px-8 text-base font-light leading-snug text-stone-700 sm:max-w-2xl sm:text-lg dark:text-stone-50">
|
||||
By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent
|
||||
this screen from appearing in the future.
|
||||
By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent this
|
||||
screen from appearing in the future.
|
||||
</p>
|
||||
<div
|
||||
class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 sm:max-w-2xl sm:flex-row"
|
||||
>
|
||||
<div class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 sm:max-w-2xl sm:flex-row">
|
||||
<button
|
||||
id="age-verification-reject"
|
||||
class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-stone-300 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50"
|
||||
|
|
@ -49,13 +43,13 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
|||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelector('#age-verification-reject')!.addEventListener('click', () => {
|
||||
window.location.href = 'about:blank'
|
||||
})
|
||||
document.querySelector('#age-verification-accept')!.addEventListener('click', () => {
|
||||
localStorage.setItem('ageVerified', 'true')
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
window.location.href = params.get('redirect') || '/'
|
||||
})
|
||||
document.querySelector("#age-verification-reject")!.addEventListener("click", () => {
|
||||
window.location.href = "about:blank";
|
||||
});
|
||||
document.querySelector("#age-verification-accept")!.addEventListener("click", () => {
|
||||
localStorage.setItem("ageVerified", "true");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
window.location.href = params.get("redirect") || "/";
|
||||
});
|
||||
</script>
|
||||
</BaseLayout>
|
||||
</BaseLayout>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,42 @@
|
|||
import rss, { type RSSFeedItem } from '@astrojs/rss'
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import rss, { type RSSFeedItem } from "@astrojs/rss";
|
||||
import type { APIRoute } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getUnixTime, addHours } from "date-fns";
|
||||
|
||||
type FeedItem = RSSFeedItem & {
|
||||
pubDate: Date
|
||||
}
|
||||
pubDate: Date;
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const stories = (await getCollection('stories')).filter(story => !story.data.isDraft)
|
||||
const games = (await getCollection('games')).filter(game => !game.data.isDraft)
|
||||
const stories = (await getCollection("stories")).filter((story) => !story.data.isDraft);
|
||||
const games = (await getCollection("games")).filter((game) => !game.data.isDraft);
|
||||
return rss({
|
||||
title: 'Gallery | Bad Manners',
|
||||
description: 'Stories, games, and artwork by Bad Manners',
|
||||
title: "Gallery | Bad Manners",
|
||||
description: "Stories, games, and more by Bad Manners",
|
||||
site: site as URL,
|
||||
items: [
|
||||
stories.map<FeedItem>((story) => ({
|
||||
title: `New story! "${story.data.title}"`,
|
||||
pubDate: addHours(story.data.pubDate, 12),
|
||||
link: `/stories/${story.slug}`,
|
||||
description: `Word count: ${story.data.wordCount}. ${story.data.contentWarning} ${story.data.descriptionPlaintext || story.data.description}`,
|
||||
categories: ['story'],
|
||||
description:
|
||||
`Word count: ${story.data.wordCount}. ${story.data.contentWarning} ${story.data.descriptionPlaintext || story.data.description}`
|
||||
.replaceAll(/\n+| +/g, " ")
|
||||
.trim(),
|
||||
categories: ["story"],
|
||||
})),
|
||||
games.map<FeedItem>((game) => ({
|
||||
title: `New game! "${game.data.title}"`,
|
||||
pubDate: addHours(game.data.pubDate, 12),
|
||||
link: `/games/${game.slug}`,
|
||||
description: `${game.data.contentWarning} ${game.data.descriptionPlaintext || game.data.description}`,
|
||||
categories: ['game'],
|
||||
description: `${game.data.contentWarning} ${game.data.descriptionPlaintext || game.data.description}`
|
||||
.replaceAll(/\n+| +/g, " ")
|
||||
.trim(),
|
||||
categories: ["game"],
|
||||
})),
|
||||
].flat().sort((a, b) => getUnixTime(b.pubDate) - getUnixTime(a.pubDate)).slice(0, 10),
|
||||
})
|
||||
}
|
||||
]
|
||||
.flat()
|
||||
.sort((a, b) => getUnixTime(b.pubDate) - getUnixTime(a.pubDate))
|
||||
.slice(0, 10),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,24 +2,41 @@
|
|||
import { Image } from "astro:assets";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getUnixTime, format as formatDate } from "date-fns";
|
||||
import { enUS as enUSLocale } from "date-fns/locale/en-US"
|
||||
import { enUS as enUSLocale } from "date-fns/locale/en-US";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
|
||||
const games = (await getCollection('games')).filter(game => !game.data.isDraft).sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate))
|
||||
const games = (await getCollection("games"))
|
||||
.filter((game) => !game.data.isDraft)
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate));
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Games">
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1>
|
||||
<p class="my-4">A game that I've gone and done.</p>
|
||||
<ul class="my-6 flex justify-center md:justify-normal flex-wrap text-center gap-4">
|
||||
{games.map((game) =>
|
||||
<li>
|
||||
<a class="focus:underline hover:underline text-link" href={`/games/${game.slug}`}>
|
||||
{game.data.thumbnail &&
|
||||
<Image class="max-w-72" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={game.data.thumbnailWidth} height={game.data.thumbnailHeight} />
|
||||
}
|
||||
<div class="text-sm max-w-72"><span>{game.data.title}</span><br><span class="italic">{formatDate(game.data.pubDate, 'MMM d, yyyy', { locale: enUSLocale })}</span></div>
|
||||
</a>
|
||||
</li>)}
|
||||
<ul class="my-6 flex flex-wrap justify-center gap-4 text-center md:justify-normal">
|
||||
{
|
||||
games.map((game) => (
|
||||
<li>
|
||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
||||
{game.data.thumbnail && (
|
||||
<Image
|
||||
class="max-w-72"
|
||||
src={game.data.thumbnail}
|
||||
alt={`Thumbnail for ${game.data.title}`}
|
||||
width={game.data.thumbnailWidth}
|
||||
height={game.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<div class="max-w-72 text-sm">
|
||||
<>
|
||||
<span>{game.data.title}</span>
|
||||
<br />
|
||||
<span class="italic">{formatDate(game.data.pubDate, "MMM d, yyyy", { locale: enUSLocale })}</span>
|
||||
</>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import GameLayout from '../../layouts/GameLayout.astro';
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import GameLayout from "../../layouts/GameLayout.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const games = await getCollection('games');
|
||||
return games.map(story => ({
|
||||
const games = await getCollection("games");
|
||||
return games.map((story) => ({
|
||||
params: { slug: story.slug },
|
||||
props: story,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'games'>
|
||||
type Props = CollectionEntry<"games">;
|
||||
|
||||
const story = Astro.props;
|
||||
const { Content } = await story.render();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
---
|
||||
import GalleryLayout from '../layouts/GalleryLayout.astro'
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Gallery">
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1>
|
||||
<p class="my-4">Welcome to my gallery! Use the navigation menu to navigate through my content.</p>
|
||||
<ul class="pl-8 list-disc">
|
||||
<li><a class="underline text-link" href="/stories/1">Read my stories.</a></li>
|
||||
<li><a class="underline text-link" href="/games/crossing-over">Play my visual novel.</a></li>
|
||||
<li><a class="underline text-link" href="/tags">Find all content with a certain tag.</a></li>
|
||||
<ul class="list-disc pl-8">
|
||||
<li><a class="text-link underline" href="/stories/1">Read my stories.</a></li>
|
||||
<li><a class="text-link underline" href="/games/crossing-over">Play my visual novel.</a></li>
|
||||
<li><a class="text-link underline" href="/tags">Find all content with a certain tag.</a></li>
|
||||
</ul>
|
||||
<p class="my-4">
|
||||
For more information about me, please check out <a
|
||||
class="text-link underline"
|
||||
href="https://badmanners.xyz/"
|
||||
target="_blank">my main website</a
|
||||
>.
|
||||
</p>
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
const licenses =
|
||||
`The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
|
||||
const licenses = `The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
|
||||
|
||||
The typeface Noto Serif is copyrighted to the Noto Project Authors and is distributed under the SIL Open Font License v1.1.
|
||||
The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1.
|
||||
|
||||
The generic SVG icons were created by Font Awesome and are distributed under the CC BY 4.0 license.
|
||||
|
||||
All third-party trademarks belong to their respective owners, and I'm not affiliated with any of them.
|
||||
`
|
||||
`;
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
return new Response(licenses, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
||||
}
|
||||
export const GET: APIRoute = () => {
|
||||
return new Response(licenses, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import StoryLayout from '../../layouts/StoryLayout.astro';
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import StoryLayout from "../../layouts/StoryLayout.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const stories = await getCollection('stories');
|
||||
return stories.map(story => ({
|
||||
const stories = await getCollection("stories");
|
||||
return stories.map((story) => ({
|
||||
params: { slug: story.slug },
|
||||
props: story,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'stories'>
|
||||
type Props = CollectionEntry<"stories">;
|
||||
|
||||
const story = Astro.props;
|
||||
const { Content } = await story.render();
|
||||
|
|
|
|||
|
|
@ -3,48 +3,111 @@ import type { GetStaticPathsOptions } from "astro";
|
|||
import { Image } from "astro:assets";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getUnixTime, format as formatDate } from "date-fns";
|
||||
import { enUS as enUSLocale } from "date-fns/locale/en-US"
|
||||
import { enUS as enUSLocale } from "date-fns/locale/en-US";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
|
||||
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
||||
const stories = (await getCollection('stories')).filter(story => !story.data.isDraft).sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate))
|
||||
const stories = (await getCollection("stories"))
|
||||
.filter((story) => !story.data.isDraft)
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate));
|
||||
return paginate(stories, { pageSize: 30 });
|
||||
}
|
||||
const { page } = Astro.props
|
||||
const totalPages = Math.ceil(page.total / page.size)
|
||||
const { page } = Astro.props;
|
||||
const totalPages = Math.ceil(page.total / page.size);
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Stories">
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1>
|
||||
<p class="my-4">My main collection of content so far.</p>
|
||||
<p class="text-center font-light text-stone-950 dark:text-white">{page.start == page.end ? `Displaying story ${page.start + 1}` : `Displaying stories ${page.start + 1} - ${page.end + 1}`} / {page.total}</p>
|
||||
<div class="mx-auto mt-2 mb-6 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
|
||||
{page.url.prev && <a class="px-2 py-1 text-link underline border-r border-stone-400 dark:border-stone-500" href={page.url.prev}>Previous page</a>}
|
||||
{[...Array(totalPages).keys()].map(p => p + 1 == page.currentPage ?
|
||||
<span class="px-4 py-1 text-stone-900 dark:text-stone-50 border-r border-stone-400 dark:border-stone-500 font-semibold">{p + 1}</span> :
|
||||
<a class="px-2 py-1 text-link underline border-r border-stone-400 dark:border-stone-500" href={`./${p + 1}`}>
|
||||
{p + 1}
|
||||
</a>)}
|
||||
{page.url.next && <a class="px-2 py-1 text-link underline" href={page.url.next}>Next page</a>}
|
||||
</div>
|
||||
<ul class="flex justify-center md:justify-normal flex-wrap text-center gap-4">
|
||||
{page.data.map((story) =>
|
||||
<li>
|
||||
<a class="focus:underline hover:underline text-link" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail &&
|
||||
<Image class="w-48" src={story.data.thumbnail} alt={`Thumbnail for ${story.data.title}`} width={story.data.thumbnailWidth} height={story.data.thumbnailHeight} />
|
||||
}
|
||||
<div class="text-sm max-w-48"><span>{story.data.title}</span><br><span class="italic">{formatDate(story.data.pubDate, 'MMM d, yyyy', { locale: enUSLocale })}</span></div>
|
||||
<p class="text-center font-light text-stone-950 dark:text-white">
|
||||
{
|
||||
page.start == page.end
|
||||
? `Displaying story ${page.start + 1}`
|
||||
: `Displaying stories ${page.start + 1} - ${page.end + 1}`
|
||||
} / {page.total}
|
||||
</p>
|
||||
<div class="mx-auto mb-6 mt-2 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
|
||||
{
|
||||
page.url.prev && (
|
||||
<a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={page.url.prev}>
|
||||
Previous page
|
||||
</a>
|
||||
</li>)}
|
||||
)
|
||||
}
|
||||
{
|
||||
[...Array(totalPages).keys()].map((p) =>
|
||||
p + 1 == page.currentPage ? (
|
||||
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
|
||||
{p + 1}
|
||||
</span>
|
||||
) : (
|
||||
<a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={`./${p + 1}`}>
|
||||
{p + 1}
|
||||
</a>
|
||||
),
|
||||
)
|
||||
}
|
||||
{
|
||||
page.url.next && (
|
||||
<a class="text-link px-2 py-1 underline" href={page.url.next}>
|
||||
Next page
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<ul class="flex flex-wrap justify-center gap-4 text-center md:justify-normal">
|
||||
{
|
||||
page.data.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
alt={`Thumbnail for ${story.data.title}`}
|
||||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<div class="max-w-48 text-sm">
|
||||
<>
|
||||
<span>{story.data.title}</span>
|
||||
<br />
|
||||
<span class="italic">{formatDate(story.data.pubDate, "MMM d, yyyy", { locale: enUSLocale })}</span>
|
||||
</>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div class="mx-auto my-6 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500">
|
||||
{page.url.prev && <a class="px-2 py-1 text-link underline border-r border-stone-400 dark:border-stone-500" href={page.url.prev}>Previous page</a>}
|
||||
{[...Array(totalPages).keys()].map(p => p + 1 == page.currentPage ?
|
||||
<span class="px-4 py-1 text-stone-900 dark:text-stone-50 border-r border-stone-400 dark:border-stone-500 font-semibold">{p + 1}</span> :
|
||||
<a class="px-2 py-1 text-link underline border-r border-stone-400 dark:border-stone-500" href={`./${p + 1}`}>
|
||||
{p + 1}
|
||||
</a>)}
|
||||
{page.url.next && <a class="px-2 py-1 text-link underline" href={page.url.next}>Next page</a>}
|
||||
{
|
||||
page.url.prev && (
|
||||
<a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={page.url.prev}>
|
||||
Previous page
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
[...Array(totalPages).keys()].map((p) =>
|
||||
p + 1 == page.currentPage ? (
|
||||
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
|
||||
{p + 1}
|
||||
</span>
|
||||
) : (
|
||||
<a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={`./${p + 1}`}>
|
||||
{p + 1}
|
||||
</a>
|
||||
),
|
||||
)
|
||||
}
|
||||
{
|
||||
page.url.next && (
|
||||
<a class="text-link px-2 py-1 underline" href={page.url.next}>
|
||||
Next page
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import { Image } from "astro:assets"
|
||||
import { Image } from "astro:assets";
|
||||
import { getUnixTime } from "date-fns";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro"
|
||||
import mapImage from '../../assets/images/tlotm_map.jpg'
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import mapImage from "../../assets/images/tlotm_map.jpg";
|
||||
|
||||
const stories = (await getCollection('stories')).filter((story) => !story.data.isDraft && story.slug.startsWith('the-lost-of-the-marshes/'))
|
||||
const mainChapters = stories.filter(story => story.slug.startsWith('the-lost-of-the-marshes/chapter-')).sort((a, b) => getUnixTime(a.data.pubDate) - getUnixTime(b.data.pubDate))
|
||||
const bonusChapters = stories.filter(story => story.slug.startsWith('the-lost-of-the-marshes/bonus-')).sort((a, b) => getUnixTime(a.data.pubDate) - getUnixTime(b.data.pubDate))
|
||||
const mainChaptersWithSummaries = mainChapters.filter(story => story.data.summary)
|
||||
const stories = (await getCollection("stories")).filter(
|
||||
(story) => !story.data.isDraft && story.slug.startsWith("the-lost-of-the-marshes/"),
|
||||
);
|
||||
const mainChapters = stories
|
||||
.filter((story) => story.slug.startsWith("the-lost-of-the-marshes/chapter-"))
|
||||
.sort((a, b) => getUnixTime(a.data.pubDate) - getUnixTime(b.data.pubDate));
|
||||
const bonusChapters = stories
|
||||
.filter((story) => story.slug.startsWith("the-lost-of-the-marshes/bonus-"))
|
||||
.sort((a, b) => getUnixTime(a.data.pubDate) - getUnixTime(b.data.pubDate));
|
||||
const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summary);
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="The Lost of the Marshes">
|
||||
|
|
@ -16,47 +22,80 @@ const mainChaptersWithSummaries = mainChapters.filter(story => story.data.summar
|
|||
<p class="my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p>
|
||||
<section class="my-2" aria-labelledby="main-chapters">
|
||||
<h2 id="main-chapters" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Main chapters</h2>
|
||||
<details class="mx-3 mt-1 mb-6 rounded-lg border border-stone-400 dark:border-stone-500 bg-stone-300 dark:bg-stone-700">
|
||||
<summary class="py-1 px-2 rounded-lg bg-stone-200 dark:bg-stone-800">Click to reveal spoilers up to {mainChaptersWithSummaries[mainChaptersWithSummaries.length - 1].data.title.match(/Chapter \d+\b/)?.[0]}</summary>
|
||||
<ul class="px-1 border-t border-stone-400 dark:border-stone-500">
|
||||
{mainChapters.filter(story => story.data.summary).map(story =>
|
||||
<li class="my-2">
|
||||
<a class="underline text-link" href={`/stories/${story.slug}`}>{story.data.shortTitle || story.data.title}</a>: <span>{story.data.summary}</span>
|
||||
</li>)
|
||||
<details
|
||||
class="mx-3 mb-6 mt-1 rounded-lg border border-stone-400 bg-stone-300 dark:border-stone-500 dark:bg-stone-700"
|
||||
>
|
||||
<summary class="rounded-lg bg-stone-200 px-2 py-1 dark:bg-stone-800"
|
||||
>Click to reveal spoilers up to {
|
||||
mainChaptersWithSummaries[mainChaptersWithSummaries.length - 1].data.title.match(/Chapter \d+\b/)?.[0]
|
||||
}</summary
|
||||
>
|
||||
<ul class="border-t border-stone-400 px-1 dark:border-stone-500">
|
||||
{
|
||||
mainChapters
|
||||
.filter((story) => story.data.summary)
|
||||
.map((story) => (
|
||||
<li class="my-2">
|
||||
<a class="text-link underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.shortTitle || story.data.title}
|
||||
</a>
|
||||
: <span>{story.data.summary}</span>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</details>
|
||||
<ul class="flex justify-center md:justify-normal flex-wrap text-center gap-4">
|
||||
{mainChapters.map(story =>
|
||||
<li>
|
||||
<a class="focus:underline hover:underline text-link" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail &&
|
||||
<Image class="w-48" src={story.data.thumbnail} alt={`Thumbnail for ${story.data.title}`} width={story.data.thumbnailWidth} height={story.data.thumbnailHeight} />
|
||||
}
|
||||
<div class="text-sm max-w-48">{story.data.title}</div>
|
||||
</a>
|
||||
</li>)
|
||||
<ul class="flex flex-wrap justify-center gap-4 text-center md:justify-normal">
|
||||
{
|
||||
mainChapters.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
alt={`Thumbnail for ${story.data.title}`}
|
||||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="my-2" aria-labelledby="bonus-chapters">
|
||||
<h2 id="bonus-chapters" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Bonus chapters</h2>
|
||||
<ul class="flex justify-center md:justify-normal flex-wrap text-center gap-4">
|
||||
{bonusChapters.map(story =>
|
||||
<li>
|
||||
<a class="focus:underline hover:underline text-link" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail &&
|
||||
<Image class="w-48" src={story.data.thumbnail} alt={`Thumbnail for ${story.data.title}`} width={story.data.thumbnailWidth} height={story.data.thumbnailHeight} />
|
||||
}
|
||||
<div class="text-sm max-w-48">{story.data.title}</div>
|
||||
</a>
|
||||
</li>)
|
||||
<ul class="flex flex-wrap justify-center gap-4 text-center md:justify-normal">
|
||||
{
|
||||
bonusChapters.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
alt={`Thumbnail for ${story.data.title}`}
|
||||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="mt-2 mb-6" aria-labelledby="map">
|
||||
<section class="mb-6 mt-2" aria-labelledby="map">
|
||||
<h2 id="map" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Map</h2>
|
||||
<p class="mt-2 mb-4">Updated to include locations up to Chapter 11.</p>
|
||||
<Image class="mx-auto w-full max-w-4xl" src={mapImage} alt="A geopolitical map for the setting of The Lost of the Marshes" />
|
||||
<p class="mb-4 mt-2">Updated to include locations up to Chapter 11.</p>
|
||||
<Image
|
||||
class="mx-auto w-full max-w-4xl break-before-page"
|
||||
src={mapImage}
|
||||
alt="A geopolitical map for the setting of The Lost of the Marshes"
|
||||
/>
|
||||
</section>
|
||||
</GalleryLayout>
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
|
|
@ -1,68 +1,140 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { slug } from 'github-slugger'
|
||||
import GalleryLayout from '../layouts/GalleryLayout.astro';
|
||||
import { getCollection } from "astro:content";
|
||||
import { slug } from "github-slugger";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
|
||||
const [stories, games] = await Promise.all([getCollection('stories'), getCollection('games')]);
|
||||
const tagsSet = new Set<string>()
|
||||
const seriesList: Record<string, string> = {}
|
||||
stories.filter(story => !story.data.isDraft).forEach(story => {
|
||||
story.data.tags.forEach(tag => {
|
||||
tagsSet.add(tag)
|
||||
})
|
||||
if (story.data.series) {
|
||||
const [series, url] = Object.entries(story.data.series)[0]
|
||||
if (seriesList[series]) {
|
||||
if (seriesList[series] !== url) {
|
||||
throw new Error(`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`)
|
||||
const [stories, games] = await Promise.all([getCollection("stories"), getCollection("games")]);
|
||||
const tagsSet = new Set<string>();
|
||||
const seriesList: Record<string, string> = {};
|
||||
stories
|
||||
.filter((story) => !story.data.isDraft)
|
||||
.forEach((story) => {
|
||||
story.data.tags.forEach((tag) => {
|
||||
tagsSet.add(tag);
|
||||
});
|
||||
if (story.data.series) {
|
||||
const [series, url] = Object.entries(story.data.series)[0];
|
||||
if (seriesList[series]) {
|
||||
if (seriesList[series] !== url) {
|
||||
throw new Error(
|
||||
`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
seriesList[series] = url;
|
||||
}
|
||||
} else {
|
||||
seriesList[series] = url
|
||||
}
|
||||
}
|
||||
})
|
||||
games.filter(game => !game.data.isDraft).forEach(game => {
|
||||
game.data.tags.forEach(tag => {
|
||||
tagsSet.add(tag)
|
||||
})
|
||||
if (game.data.series) {
|
||||
const [series, url] = Object.entries(game.data.series)[0]
|
||||
if (seriesList[series]) {
|
||||
if (seriesList[series] !== url) {
|
||||
throw new Error(`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`)
|
||||
});
|
||||
games
|
||||
.filter((game) => !game.data.isDraft)
|
||||
.forEach((game) => {
|
||||
game.data.tags.forEach((tag) => {
|
||||
tagsSet.add(tag);
|
||||
});
|
||||
if (game.data.series) {
|
||||
const [series, url] = Object.entries(game.data.series)[0];
|
||||
if (seriesList[series]) {
|
||||
if (seriesList[series] !== url) {
|
||||
throw new Error(
|
||||
`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
seriesList[series] = url;
|
||||
}
|
||||
} else {
|
||||
seriesList[series] = url
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const categorizedTags: Record<string, string[]> = {
|
||||
'Types of vore': ['oral vore', 'anal vore', 'cock vore', 'unbirth', 'tail vore', 'slit vore', 'sheath vore', 'nipple vore', 'chest maw vore'],
|
||||
'Body types': ['anthro predator', 'feral predator', 'taur predator', 'ambiguous predator', 'human prey', 'anthro prey', 'feral prey', 'ambiguous prey'],
|
||||
'Genders': ['male predator', 'trans male predator', 'female predator', 'non-binary predator', 'ambiguous gender predator', 'male prey', 'female prey', 'trans female prey', 'non-binary prey', 'ambiguous gender prey'],
|
||||
'Relative size': ['macro predator', 'micro prey', 'size difference', 'similar size', 'same size', 'smaller predator'],
|
||||
'Willingness': ['willing predator', 'semi-willing predator', 'unwilling predator', 'asleep predator', 'willing prey', 'semi-willing prey', 'unwilling prey', 'asleep prey'],
|
||||
'Vore-related scenarios': ['point of view', 'regurgitation', 'long-term endo', 'perma endo', 'implied perma endo', 'full tour', 'implied full tour', 'prey transfer', 'object vore', 'role reversal', 'nested vore', 'multiple prey', 'messy stomach', 'bladder vore', 'soul vore'],
|
||||
'Sexual content': ['nudity', 'masturbation', 'straight sex', 'gay sex', 'lesbian sex', 'orgy'],
|
||||
'Other kinks': ['hyper', 'egg play', 'transformation', 'netorare', 'sizeplay', 'inflation', 'daddy play', 'BDSM', 'dubcon'],
|
||||
'Type of content': ['request', 'commission', 'flash fiction'],
|
||||
'Recurring characters': ['Sam Brendan', 'Beetle', 'Muno'],
|
||||
}
|
||||
"Types of vore": [
|
||||
"oral vore",
|
||||
"anal vore",
|
||||
"cock vore",
|
||||
"unbirth",
|
||||
"tail vore",
|
||||
"slit vore",
|
||||
"sheath vore",
|
||||
"nipple vore",
|
||||
"chest maw vore",
|
||||
],
|
||||
"Body types": [
|
||||
"anthro predator",
|
||||
"feral predator",
|
||||
"taur predator",
|
||||
"ambiguous predator",
|
||||
"human prey",
|
||||
"anthro prey",
|
||||
"feral prey",
|
||||
"ambiguous prey",
|
||||
],
|
||||
Genders: [
|
||||
"male predator",
|
||||
"trans male predator",
|
||||
"female predator",
|
||||
"non-binary predator",
|
||||
"ambiguous gender predator",
|
||||
"male prey",
|
||||
"female prey",
|
||||
"trans female prey",
|
||||
"non-binary prey",
|
||||
"ambiguous gender prey",
|
||||
],
|
||||
"Relative size": ["macro predator", "micro prey", "size difference", "similar size", "same size", "smaller predator"],
|
||||
Willingness: [
|
||||
"willing predator",
|
||||
"semi-willing predator",
|
||||
"unwilling predator",
|
||||
"asleep predator",
|
||||
"willing prey",
|
||||
"semi-willing prey",
|
||||
"unwilling prey",
|
||||
"asleep prey",
|
||||
],
|
||||
"Vore-related scenarios": [
|
||||
"point of view",
|
||||
"regurgitation",
|
||||
"long-term endo",
|
||||
"perma endo",
|
||||
"implied perma endo",
|
||||
"full tour",
|
||||
"implied full tour",
|
||||
"prey transfer",
|
||||
"object vore",
|
||||
"role reversal",
|
||||
"nested vore",
|
||||
"multiple prey",
|
||||
"messy stomach",
|
||||
"bladder vore",
|
||||
"soul vore",
|
||||
],
|
||||
"Sexual content": ["nudity", "masturbation", "straight sex", "gay sex", "lesbian sex", "orgy"],
|
||||
"Other kinks": [
|
||||
"hyper",
|
||||
"egg play",
|
||||
"transformation",
|
||||
"netorare",
|
||||
"sizeplay",
|
||||
"inflation",
|
||||
"daddy play",
|
||||
"BDSM",
|
||||
"dubcon",
|
||||
],
|
||||
"Type of content": ["request", "commission", "flash fiction"],
|
||||
"Recurring characters": ["Sam Brendan", "Beetle", "Muno"],
|
||||
};
|
||||
|
||||
Object.entries(categorizedTags).forEach(([category, tagList]) => {
|
||||
tagList.forEach(tag => {
|
||||
tagList.forEach((tag) => {
|
||||
if (!tagsSet.delete(tag)) {
|
||||
throw new Error(`Tag "${tag}" was added to category "${category}" but isn't present in any content`)
|
||||
throw new Error(`Tag "${tag}" was added to category "${category}" but isn't present in any content`);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
if (tagsSet.size > 0) {
|
||||
console.log("The following tags have no category:", [...tagsSet])
|
||||
categorizedTags['Uncategorized tags'] = [...tagsSet]
|
||||
console.log("The following tags have no category:", [...tagsSet]);
|
||||
categorizedTags["Uncategorized tags"] = [...tagsSet];
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle={`Tags`}>
|
||||
|
|
@ -70,16 +142,34 @@ if (tagsSet.size > 0) {
|
|||
<p class="my-4">You can find all content with a specific tag by selecting it below from the appropriate category.</p>
|
||||
<section class="my-2" aria-labelledby="category-series">
|
||||
<h2 id="category-series" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Series</h2>
|
||||
<ul class="pl-8 list-disc">
|
||||
{Object.entries(seriesList).map(([series, url]) => <li><a class="underline text-link" href={url}>{series}</a></li>)}
|
||||
<ul class="list-disc pl-8">
|
||||
{
|
||||
Object.entries(seriesList).map(([series, url]) => (
|
||||
<li>
|
||||
<a class="text-link underline" href={url}>
|
||||
{series}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
{Object.entries(categorizedTags).map(([category, tagList]) =>
|
||||
<section class="my-2" aria-labelledby={`category-${slug(category)}`}>
|
||||
<h2 id={`category-${slug(category)}`} class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">{category}</h2>
|
||||
<ul class="px-2 flex flex-wrap gap-x-2 gap-y-2 max-w-3xl">
|
||||
{tagList.map(tag => <li class="text-sm rounded-full shadow-sm px-3 py-1 dark:bg-bm-600 dark:text-white bg-bm-300 text-black"><a class="focus:underline hover:underline" href={`/tags/${slug(tag)}`}>{tag}</a></li>)}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{
|
||||
Object.entries(categorizedTags).map(([category, tagList]) => (
|
||||
<section class="my-2" aria-labelledby={`category-${slug(category)}`}>
|
||||
<h2 id={`category-${slug(category)}`} class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{category}
|
||||
</h2>
|
||||
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tagList.map((tag) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`}>
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
|
|
@ -1,76 +1,102 @@
|
|||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import { slug } from 'github-slugger'
|
||||
import { Image } from "astro:assets";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { slug } from "github-slugger";
|
||||
import { getUnixTime } from "date-fns";
|
||||
import GalleryLayout from '../../layouts/GalleryLayout.astro';
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const [stories, games] = await Promise.all([getCollection('stories'), getCollection('games')]);
|
||||
const tags = new Set<string>()
|
||||
stories.forEach(story => {
|
||||
story.data.tags.forEach(tag => {
|
||||
tags.add(tag)
|
||||
})
|
||||
})
|
||||
games.forEach(game => {
|
||||
game.data.tags.forEach(tag => {
|
||||
tags.add(tag)
|
||||
})
|
||||
})
|
||||
return [...tags].filter(tag => !['The Lost of the Marshes'].includes(tag)).map(tag => ({
|
||||
params: { slug: slug(tag) },
|
||||
props: {
|
||||
tag,
|
||||
stories: stories.filter(story => !story.data.isDraft && story.data.tags.includes(tag)).sort((a, b) => getUnixTime(b.data.pubDate!) - getUnixTime(a.data.pubDate!)),
|
||||
games: games.filter(game => !game.data.isDraft && game.data.tags.includes(tag)).sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)),
|
||||
},
|
||||
}));
|
||||
const [stories, games] = await Promise.all([getCollection("stories"), getCollection("games")]);
|
||||
const tags = new Set<string>();
|
||||
stories.forEach((story) => {
|
||||
story.data.tags.forEach((tag) => {
|
||||
tags.add(tag);
|
||||
});
|
||||
});
|
||||
games.forEach((game) => {
|
||||
game.data.tags.forEach((tag) => {
|
||||
tags.add(tag);
|
||||
});
|
||||
});
|
||||
return [...tags]
|
||||
.filter((tag) => !["The Lost of the Marshes"].includes(tag))
|
||||
.map((tag) => ({
|
||||
params: { slug: slug(tag) },
|
||||
props: {
|
||||
tag,
|
||||
stories: stories
|
||||
.filter((story) => !story.data.isDraft && story.data.tags.includes(tag))
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate!) - getUnixTime(a.data.pubDate!)),
|
||||
games: games
|
||||
.filter((game) => !game.data.isDraft && game.data.tags.includes(tag))
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
type Props = {
|
||||
tag: string
|
||||
stories: CollectionEntry<'stories'>[]
|
||||
games: CollectionEntry<'games'>[]
|
||||
}
|
||||
tag: string;
|
||||
stories: CollectionEntry<"stories">[];
|
||||
games: CollectionEntry<"games">[];
|
||||
};
|
||||
|
||||
const { tag, stories, games } = Astro.props
|
||||
const { tag, stories, games } = Astro.props;
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
||||
{stories.length > 0 &&
|
||||
<section class="my-2" aria-labelledby="content-stories">
|
||||
<h2 id="content-stories" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Stories</h2>
|
||||
<ul class="flex justify-center md:justify-normal flex-wrap text-center gap-4">
|
||||
{stories.map(story =>
|
||||
<li>
|
||||
<a class="focus:underline hover:underline text-link" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail &&
|
||||
<Image class="w-48" src={story.data.thumbnail} alt={`Thumbnail for ${story.data.title}`} width={story.data.thumbnailWidth} height={story.data.thumbnailHeight} />
|
||||
}
|
||||
<div class="text-sm max-w-48">{story.data.title}</div>
|
||||
</a>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
{
|
||||
stories.length > 0 && (
|
||||
<section class="my-2" aria-labelledby="content-stories">
|
||||
<h2 id="content-stories" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
Stories
|
||||
</h2>
|
||||
<ul class="flex flex-wrap justify-center gap-4 text-center md:justify-normal">
|
||||
{stories.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
alt={`Thumbnail for ${story.data.title}`}
|
||||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
{games.length > 0 &&
|
||||
<section class="my-2" aria-labelledby="content-games">
|
||||
<h2 id="content-games" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Games</h2>
|
||||
<ul class="flex justify-center md:justify-normal flex-wrap text-center gap-4">
|
||||
{games.map(game =>
|
||||
<li>
|
||||
<a class="focus:underline hover:underline text-link" href={`/games/${game.slug}`}>
|
||||
{game.data.thumbnail &&
|
||||
<Image class="w-48" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={game.data.thumbnailWidth} height={game.data.thumbnailHeight} />
|
||||
}
|
||||
<div class="text-sm max-w-48">{game.data.title}</div>
|
||||
</a>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
{
|
||||
games.length > 0 && (
|
||||
<section class="my-2" aria-labelledby="content-games">
|
||||
<h2 id="content-games" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
Games
|
||||
</h2>
|
||||
<ul class="flex flex-wrap justify-center gap-4 text-center md:justify-normal">
|
||||
{games.map((game) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
||||
{game.data.thumbnail && (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={game.data.thumbnail}
|
||||
alt={`Thumbnail for ${game.data.title}`}
|
||||
width={game.data.thumbnailWidth}
|
||||
height={game.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
<div class="max-w-48 text-sm">{game.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</GalleryLayout>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue