Fix up first version and add Prettier and Docker

This commit is contained in:
Bad Manners 2024-03-20 11:34:09 -03:00
parent 09a1919d36
commit 324050ee38
87 changed files with 2210 additions and 822 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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),
});
};

View file

@ -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>

View file

@ -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();

View file

@ -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>

View file

@ -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" },
});
};

View file

@ -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();

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>