Add "Playing It Safe" draft and improve type-checking
- Remove most type assertions and improve types - Validate wordCount property - Add "Vore Day" tag - Add licenses
This commit is contained in:
parent
17ef8c652c
commit
fe908a4989
37 changed files with 1309 additions and 841 deletions
|
|
@ -84,7 +84,8 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
|
|||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Website "${website}"`);
|
||||
let _: never = website;
|
||||
throw new Error(`Unhandled website "${website}"`);
|
||||
}
|
||||
} else {
|
||||
return link[1].replace(/^@/, "");
|
||||
|
|
@ -174,9 +175,9 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
|
||||
const { lang } = story.data;
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
|
||||
const authorsList = await getEntries([story.data.authors].flat());
|
||||
const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
|
||||
const requester = story.data.requester && (await getEntry(story.data.requester));
|
||||
const authorsList = await getEntries(story.data.authors);
|
||||
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner));
|
||||
const requestersList = story.data.requester && (await getEntries(story.data.requester));
|
||||
|
||||
const description = Object.fromEntries(
|
||||
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
||||
|
|
@ -185,14 +186,24 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
const storyDescription = (
|
||||
[
|
||||
story.data.description,
|
||||
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}*`,
|
||||
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
||||
t(
|
||||
lang,
|
||||
"export_story/writing",
|
||||
authorsList.map((author) => u(author)),
|
||||
),
|
||||
requester && t(lang, "export_story/request_for", u(requester)),
|
||||
commissioner && t(lang, "export_story/commissioned_by", u(commissioner)),
|
||||
requestersList &&
|
||||
t(
|
||||
lang,
|
||||
"export_story/request_for",
|
||||
requestersList.map((requester) => u(requester)),
|
||||
),
|
||||
commissionersList &&
|
||||
t(
|
||||
lang,
|
||||
"export_story/commissioned_by",
|
||||
commissionersList.map((commissioner) => u(commissioner)),
|
||||
),
|
||||
...copyrightedCharacters.map(([user, characterList]) =>
|
||||
characterList[0] == ""
|
||||
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
||||
|
|
@ -206,12 +217,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||
);
|
||||
if (exportFormat === "bbcode") {
|
||||
return [
|
||||
website,
|
||||
markdownToBbcode(storyDescription)
|
||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||
.trim(),
|
||||
];
|
||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
||||
} else if (exportFormat === "markdown") {
|
||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
} else {
|
||||
|
|
@ -227,8 +233,20 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
"story/authors",
|
||||
authorsList.map((author) => getUsernameForLang(author, lang)),
|
||||
)}\n` +
|
||||
(commissioner ? `${t(lang, "story/commissioned_by", getUsernameForLang(commissioner, lang))}\n` : "") +
|
||||
(requester ? `${t(lang, "story/requested_by", getUsernameForLang(requester, lang))}\n` : "");
|
||||
(requestersList
|
||||
? `${t(
|
||||
lang,
|
||||
"story/requested_by",
|
||||
requestersList.map((requester) => getUsernameForLang(requester, lang)),
|
||||
)}\n`
|
||||
: "") +
|
||||
(commissionersList
|
||||
? `${t(
|
||||
lang,
|
||||
"story/commissioned_by",
|
||||
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
|
||||
)}\n`
|
||||
: "");
|
||||
|
||||
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
|
||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import type { APIRoute } from "astro";
|
||||
|
||||
const content = { isAlive: true };
|
||||
|
||||
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
if (import.meta.env.PROD) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
return new Response(JSON.stringify({ isAlive: true }), {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
return new Response(JSON.stringify(content), { headers });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import rss, { type RSSFeedItem } from "@astrojs/rss";
|
||||
import type { APIRoute } from "astro";
|
||||
import { getCollection, type CollectionEntry } from "astro:content";
|
||||
import { getCollection, getEntries, type CollectionEntry, type CollectionKey } from "astro:content";
|
||||
import { markdown } from "@astropub/md";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { t } from "../i18n";
|
||||
|
|
@ -8,11 +8,12 @@ import type { Lang } from "../content/config";
|
|||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||
|
||||
type FeedItem = RSSFeedItem & {
|
||||
pubDate: Date;
|
||||
};
|
||||
type FeedItem = RSSFeedItem &
|
||||
Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>;
|
||||
|
||||
const MAX_ITEMS = 8;
|
||||
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
||||
|
||||
const MAX_ITEMS = 6;
|
||||
|
||||
function toNoonUTCDate(date: Date) {
|
||||
const adjustedDate = new Date(date);
|
||||
|
|
@ -30,13 +31,19 @@ const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
|
|||
};
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const stories = (await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate))
|
||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
|
||||
const stories = (
|
||||
(await getCollection(
|
||||
"stories",
|
||||
(story) => !story.data.isDraft && story.data.pubDate,
|
||||
)) as EntryWithPubDate<"stories">[]
|
||||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate))
|
||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
|
||||
const games = (
|
||||
(await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as EntryWithPubDate<"games">[]
|
||||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
const users = await getCollection("users");
|
||||
|
||||
return rss({
|
||||
title: "Gallery | Bad Manners",
|
||||
|
|
@ -46,34 +53,38 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
await Promise.all(
|
||||
stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
|
||||
title: `New story! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate!),
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/stories/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())}\n\n${markdownToPlaintext(data.description)}`
|
||||
.replaceAll(/[\n ]+/g, " ")
|
||||
.trim(),
|
||||
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["story"],
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
[data.authors].flatMap((authorArray) => {
|
||||
if (!Array.isArray(authorArray)) {
|
||||
authorArray = [authorArray];
|
||||
}
|
||||
return authorArray.map((author) =>
|
||||
getLinkForUser(users.find((user) => user.id === author.id)!, data.lang),
|
||||
);
|
||||
}),
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
(data.requester
|
||||
? `<p>${t(data.lang, "export_story/request_for", getLinkForUser(users.find((user) => user.id === data.requester!.id)!, data.lang))}</p>`
|
||||
? `<p>${t(
|
||||
data.lang,
|
||||
"story/requested_by",
|
||||
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)),
|
||||
)}</p>`
|
||||
: "") +
|
||||
(data.commissioner
|
||||
? `<p>${t(data.lang, "export_story/commissioned_by", getLinkForUser(users.find((user) => user.id === data.commissioner!.id)!, data.lang))}</p>`
|
||||
? `<p>${t(
|
||||
data.lang,
|
||||
"story/commissioned_by",
|
||||
(await getEntries(data.commissioner)).map((commissioner) =>
|
||||
getLinkForUser(commissioner, data.lang),
|
||||
),
|
||||
)}</p>`
|
||||
: "") +
|
||||
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount || "???", data.contentWarning.trim())}</em></p>` +
|
||||
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +
|
||||
`<hr>${await markdown(body)}` +
|
||||
`<hr>${await markdown(data.description)}`,
|
||||
),
|
||||
|
|
@ -82,29 +93,23 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
await Promise.all(
|
||||
games.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
|
||||
title: `New game! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate!),
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/games/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`
|
||||
.replaceAll(/[\n ]+/g, " ")
|
||||
.trim(),
|
||||
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["game"],
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
[data.authors].flatMap((authorArray) => {
|
||||
if (!Array.isArray(authorArray)) {
|
||||
authorArray = [authorArray];
|
||||
}
|
||||
return authorArray.map((author) =>
|
||||
getLinkForUser(users.find((user) => user.id === author.id)!, data.lang),
|
||||
);
|
||||
}),
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
||||
`<hr><p><em>${data.contentWarning.trim()}</em></p>` +
|
||||
`<hr><p><em>${data.contentWarning}</em></p>` +
|
||||
`<hr>${await markdown(body)}` +
|
||||
`<hr>${await markdown(data.description)}`,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getCollection, type CollectionEntry } from "astro:content";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
|
||||
const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)).sort(
|
||||
(a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(),
|
||||
);
|
||||
type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } };
|
||||
|
||||
const games = (
|
||||
(await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as GameWithPubDate[]
|
||||
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Games">
|
||||
|
|
@ -20,7 +22,7 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game
|
|||
<a
|
||||
class="text-link hover:underline focus:underline"
|
||||
href={`/games/${game.slug}`}
|
||||
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())}
|
||||
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
|
||||
>
|
||||
{game.data.thumbnail ? (
|
||||
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
|
||||
|
|
@ -32,7 +34,7 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game
|
|||
<span>{game.data.title}</span>
|
||||
<br />
|
||||
<span class="italic">
|
||||
{game.data.pubDate!.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
</span>
|
||||
</>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
};
|
||||
|
||||
const game = Astro.props;
|
||||
if (!game.data.isDraft) {
|
||||
if (!game.data.pubDate) {
|
||||
throw new Error(`Missing "pubDate" for published game ${game.data.title} ("${game.slug}")`);
|
||||
}
|
||||
if (!game.data.thumbnail) {
|
||||
throw new Error(`Missing "thumbnail" for published game ${game.data.title} ("${game.slug}")`);
|
||||
}
|
||||
if (game.data.tags.length == 0) {
|
||||
throw new Error(`Missing "tags" for published game ${game.data.title} ("${game.slug}")`);
|
||||
}
|
||||
}
|
||||
const { Content } = await game.render();
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
|
||||
import { Image } from "astro:assets";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
|
|
@ -15,11 +15,20 @@ interface LatestItemsEntry {
|
|||
pubDate: Date;
|
||||
}
|
||||
|
||||
const stories = (await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate))
|
||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
|
||||
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
||||
|
||||
const stories = (
|
||||
(await getCollection(
|
||||
"stories",
|
||||
(story) => !story.data.isDraft && story.data.pubDate,
|
||||
)) as EntryWithPubDate<"stories">[]
|
||||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate))
|
||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime())
|
||||
const games = (
|
||||
(await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as EntryWithPubDate<"games">[]
|
||||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
|
||||
const latestItems: LatestItemsEntry[] = [
|
||||
|
|
@ -28,16 +37,16 @@ const latestItems: LatestItemsEntry[] = [
|
|||
thumbnail: story.data.thumbnail,
|
||||
href: `/stories/${story.slug}`,
|
||||
title: story.data.title,
|
||||
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
|
||||
pubDate: story.data.pubDate!,
|
||||
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning),
|
||||
pubDate: story.data.pubDate,
|
||||
})),
|
||||
games.map<LatestItemsEntry>((game) => ({
|
||||
type: "Game",
|
||||
thumbnail: game.data.thumbnail,
|
||||
href: `/games/${game.slug}`,
|
||||
title: game.data.title,
|
||||
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim()),
|
||||
pubDate: game.data.pubDate!,
|
||||
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning),
|
||||
pubDate: game.data.pubDate,
|
||||
})),
|
||||
]
|
||||
.flat()
|
||||
|
|
@ -48,11 +57,18 @@ const latestItems: LatestItemsEntry[] = [
|
|||
<GalleryLayout pageTitle="Gallery">
|
||||
<meta slot="head-description" property="og:description" content="Bad Manners || Welcome to my 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>
|
||||
<p class="my-4 block md:hidden">
|
||||
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu above to navigate
|
||||
through my content.
|
||||
</p>
|
||||
<p class="my-4 hidden md:block" aria-hidden>
|
||||
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to the left to
|
||||
navigate through my content.
|
||||
</p>
|
||||
<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>
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import type { APIRoute } from "astro";
|
|||
const licenses = `
|
||||
The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
|
||||
|
||||
The source code of this website is licensed under the MIT License. The content hosted on it (i.e. stories, games, and respective thumbnails/images) is copyrighted by me and distributed under the CC BY-NC-ND 4.0 license.
|
||||
|
||||
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.
|
||||
|
|
@ -10,8 +12,8 @@ The generic SVG icons were created by Font Awesome and are distributed under the
|
|||
All third-party trademarks and attributed characters belong to their respective owners, and I'm not affiliated with any of them.
|
||||
`.trim();
|
||||
|
||||
const headers = { "Content-Type": "text/plain; charset=utf-8" };
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
return new Response(licenses, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
return new Response(licenses, { headers });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import getReadingTime from "reading-time";
|
||||
import StoryLayout from "../../layouts/StoryLayout.astro";
|
||||
|
||||
type Props = CollectionEntry<"stories">;
|
||||
|
|
@ -18,6 +19,28 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
};
|
||||
|
||||
const story = Astro.props;
|
||||
const readingTime = getReadingTime(story.body);
|
||||
if (!story.data.isDraft) {
|
||||
if (!story.data.wordCount) {
|
||||
throw new Error(`Missing "wordCount" for published story ${story.data.title} ("${story.slug}")`);
|
||||
}
|
||||
if (!story.data.pubDate) {
|
||||
throw new Error(`Missing "pubDate" for published story ${story.data.title} ("${story.slug}")`);
|
||||
}
|
||||
if (!story.data.thumbnail) {
|
||||
throw new Error(`Missing "thumbnail" for published story ${story.data.title} ("${story.slug}")`);
|
||||
}
|
||||
if (story.data.tags.length == 0) {
|
||||
throw new Error(`Missing "tags" for published story ${story.data.title} ("${story.slug}")`);
|
||||
}
|
||||
}
|
||||
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 135) {
|
||||
console.warn(
|
||||
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
|
||||
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
||||
);
|
||||
}
|
||||
|
||||
const { Content } = await story.render();
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
|||
import type { CollectionEntry } from "astro:content";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
|
||||
|
||||
type Props = {
|
||||
page: Page<CollectionEntry<"stories">>;
|
||||
page: Page<StoryWithPubDate>;
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
|
||||
const stories = (await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)).sort(
|
||||
(a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(),
|
||||
);
|
||||
const stories = (
|
||||
(await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)) as StoryWithPubDate[]
|
||||
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
|
||||
return paginate(stories, { pageSize: 30 }) satisfies { props: Props }[];
|
||||
};
|
||||
|
||||
|
|
@ -71,7 +73,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
<a
|
||||
class="text-link hover:underline focus:underline"
|
||||
href={`/stories/${story.slug}`}
|
||||
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}
|
||||
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
|
||||
>
|
||||
{story.data.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
|
|
@ -87,7 +89,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
<span>{story.data.title}</span>
|
||||
<br />
|
||||
<span class="italic">
|
||||
{story.data.pubDate!.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
{story.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
---
|
||||
import { getCollection, getEntry } from "astro:content";
|
||||
import { getCollection, getEntry, type CollectionEntry } from "astro:content";
|
||||
import { Image } from "astro:assets";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import mapImage from "../../assets/images/tlotm_map.jpg";
|
||||
|
||||
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
|
||||
|
||||
const series = await getEntry("series", "the-lost-of-the-marshes");
|
||||
const stories = await getCollection(
|
||||
const stories = (await getCollection(
|
||||
"stories",
|
||||
(story) => !story.data.isDraft && story.data.pubDate && story.data.series?.id === series.id,
|
||||
);
|
||||
)) as StoryWithPubDate[];
|
||||
const mainChapters = stories
|
||||
.filter((story) => story.slug.startsWith("the-lost-of-the-marshes/chapter-"))
|
||||
.sort((a, b) => a.data.pubDate!.getTime() - b.data.pubDate!.getTime());
|
||||
.sort((a, b) => a.data.pubDate.getTime() - b.data.pubDate.getTime());
|
||||
const bonusChapters = stories
|
||||
.filter((story) => story.slug.startsWith("the-lost-of-the-marshes/bonus-"))
|
||||
.sort((a, b) => a.data.pubDate!.getTime() - b.data.pubDate!.getTime());
|
||||
.sort((a, b) => a.data.pubDate.getTime() - b.data.pubDate.getTime());
|
||||
const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summary);
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const categorizedTags = tagCategories
|
|||
})
|
||||
.map((category) => {
|
||||
const tagList = category.data.tags.map<Tag>(({ name, description }) => {
|
||||
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ").trim();
|
||||
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ");
|
||||
const tag = typeof name === "string" ? name : name["eng"];
|
||||
const id = slug(tag);
|
||||
return { id, name: tag, description };
|
||||
|
|
|
|||
28
src/pages/tags/[badSlug].astro
Normal file
28
src/pages/tags/[badSlug].astro
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { slug } from "github-slugger";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
|
||||
type Props = {
|
||||
badTag: string;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
badSlug: string;
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return ["digestion", "fatal vore"].map((badTag) => ({
|
||||
params: { badSlug: slug(badTag) } satisfies Params,
|
||||
props: { badTag } satisfies Props,
|
||||
}));
|
||||
};
|
||||
|
||||
const { badTag } = Astro.props;
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle={`Works tagged "${badTag}"`}>
|
||||
<meta slot="head-description" content="No." property="og:description" />
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{badTag}"</h1>
|
||||
<p class="my-4">No.</p>
|
||||
</GalleryLayout>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
|
|
@ -9,12 +9,14 @@ import Prose from "../../components/Prose.astro";
|
|||
import { t } from "../../i18n";
|
||||
import { DEFAULT_LANG } from "../../i18n";
|
||||
|
||||
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
||||
|
||||
type Props = {
|
||||
tag: string;
|
||||
description?: string;
|
||||
related?: string[];
|
||||
stories: CollectionEntry<"stories">[];
|
||||
games: CollectionEntry<"games">[];
|
||||
stories: EntryWithPubDate<"stories">[];
|
||||
games: EntryWithPubDate<"games">[];
|
||||
};
|
||||
|
||||
type Params = {
|
||||
|
|
@ -74,12 +76,16 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
tag,
|
||||
description: tagDescriptions[tag]?.description,
|
||||
related: tagDescriptions[tag]?.related,
|
||||
stories: stories
|
||||
.filter((story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag))
|
||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()),
|
||||
games: games
|
||||
.filter((game) => !game.data.isDraft && game.data.pubDate && game.data.tags.includes(tag))
|
||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()),
|
||||
stories: (
|
||||
stories.filter(
|
||||
(story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag),
|
||||
) as EntryWithPubDate<"stories">[]
|
||||
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()),
|
||||
games: (
|
||||
games.filter(
|
||||
(game) => !game.data.isDraft && game.data.pubDate && game.data.tags.includes(tag),
|
||||
) as EntryWithPubDate<"games">[]
|
||||
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()),
|
||||
} satisfies Props,
|
||||
}));
|
||||
};
|
||||
|
|
@ -119,7 +125,7 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
|||
<a
|
||||
class="text-link hover:underline focus:underline"
|
||||
href={`/stories/${story.slug}`}
|
||||
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}
|
||||
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
|
||||
>
|
||||
{story.data.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
|
|
@ -151,7 +157,7 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
|||
<a
|
||||
class="text-link hover:underline focus:underline"
|
||||
href={`/games/${game.slug}`}
|
||||
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())}
|
||||
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
|
||||
>
|
||||
{game.data.thumbnail ? (
|
||||
<div class="flex aspect-[630/500] max-w-[192px] justify-center">
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
|
||||
const tag = "Digestion";
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
||||
<meta slot="head-description" content="No." property="og:description" />
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
||||
<p class="my-4">No.</p>
|
||||
</GalleryLayout>
|
||||
Loading…
Add table
Add a link
Reference in a new issue