Improve schema and update tags

- Make constants in schema explicit
- Enumerate tag-categories
- More i18n utilities
- Better anonymous user support without special field
- Remove most tuples and unchecked type-casting
This commit is contained in:
Bad Manners 2024-07-26 15:48:57 -03:00
parent 579e5879e1
commit 17ef8c652c
34 changed files with 223 additions and 221 deletions

View file

@ -1,10 +1,11 @@
import type { APIRoute, GetStaticPaths } from "astro";
import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
import type { Lang, Website } from "../../../content/config";
import type { Website } from "../../../content/config";
import { t } from "../../../i18n";
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
import { getUsernameForLang } from "../../../utils/get_username_for_lang";
import { isAnonymousUser } from "../../../utils/is_anonymous_user";
interface ExportWebsiteInfo {
website: Website;
@ -97,10 +98,7 @@ function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): b
return !preferredLink || preferredLink == website;
}
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName, anonymousFallback: string): string {
if (user.data.isAnonymous) {
return anonymousFallback;
}
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName): string {
switch (website) {
case "eka":
if ("eka" in user.data.links) {
@ -155,13 +153,6 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
);
}
function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string {
if (user.data.isAnonymous) {
return getUsernameForLang(anonymousUser, lang);
}
return getUsernameForLang(user, lang);
}
type Props = {
story: CollectionEntry<"stories">;
};
@ -180,23 +171,21 @@ export const getStaticPaths: GetStaticPaths = async () => {
}));
};
const ANONYMOUS_USER = await getEntry("users", "anonymous");
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 anonymousFallback = getNameForUser(ANONYMOUS_USER, ANONYMOUS_USER, lang);
const description = Object.fromEntries(
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
const u = (user: CollectionEntry<"users">) =>
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website);
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.trim())}*`,
t(
lang,
"export_story/writing",
@ -236,10 +225,10 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
`${t(
lang,
"story/authors",
authorsList.map((author) => getNameForUser(author, ANONYMOUS_USER, lang)),
authorsList.map((author) => getUsernameForLang(author, lang)),
)}\n` +
(commissioner ? `${t(lang, "story/commissioned_by", getNameForUser(commissioner, ANONYMOUS_USER, lang))}\n` : "") +
(requester ? `${t(lang, "story/requested_by", getNameForUser(requester, ANONYMOUS_USER, lang))}\n` : "");
(commissioner ? `${t(lang, "story/commissioned_by", getUsernameForLang(commissioner, lang))}\n` : "") +
(requester ? `${t(lang, "story/requested_by", getUsernameForLang(requester, lang))}\n` : "");
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
.replaceAll(/\n\n\n+/g, "\n\n")

View file

@ -49,7 +49,7 @@ export const GET: APIRoute = async ({ site }) => {
pubDate: toNoonUTCDate(data.pubDate!),
link: `/stories/${slug}`,
description:
`${t(data.lang, "story/warnings", data.wordCount || "???", data.contentWarning.trim())}\n\n${markdownToPlaintext(data.description)}`
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())}\n\n${markdownToPlaintext(data.description)}`
.replaceAll(/[\n ]+/g, " ")
.trim(),
categories: ["story"],

View file

@ -28,7 +28,7 @@ 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()),
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
pubDate: story.data.pubDate!,
})),
games.map<LatestItemsEntry>((game) => ({

View file

@ -71,12 +71,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.trim())}
>
{story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center">

View file

@ -4,6 +4,12 @@ import { slug } from "github-slugger";
import GalleryLayout from "../layouts/GalleryLayout.astro";
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
interface Tag {
id: string;
name: string;
description?: string;
}
const [stories, games, tagCategories] = await Promise.all([
getCollection("stories"),
getCollection("games"),
@ -32,24 +38,30 @@ const seriesCollection = await getCollection("series");
const uncategorizedTagsSet = new Set(tagsSet);
const categorizedTags = tagCategories
.sort((a, b) => a.data.index - b.data.index)
.sort((a, b) => {
if (a.data.index == b.data.index) {
throw new Error(
`Found tag categories with same index value ${a.data.index} ("${a.data.name}", "${b.data.name}")`,
);
}
return a.data.index - b.data.index;
})
.map((category) => {
const tagList = category.data.tags.map(({ name, description }) => {
const tagList = category.data.tags.map<Tag>(({ name, description }) => {
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ").trim();
return (typeof name === "string" ? { name, description } : { name: name["eng"]!, description }) as {
name: string;
description?: string;
};
const tag = typeof name === "string" ? name : name["eng"];
const id = slug(tag);
return { id, name: tag, description };
});
tagList.forEach(({ name }, index) => {
if (index !== tagList.findLastIndex(({ name: otherTag }) => name == otherTag)) {
throw new Error(`Duplicated tag "${name}" found in multiple tag-categories lists`);
}
});
return [
category.data.name,
category.id,
tagList.filter(({ name }) => {
return {
name: category.data.name,
id: category.id,
tags: tagList.filter(({ name }) => {
if (draftOnlyTagsSet.has(name)) {
console.log(`Omitting draft-only tag "${name}"`);
return false;
@ -59,7 +71,7 @@ const categorizedTags = tagCategories
}
return true;
}),
] as [string, string, { name: string; description?: string }[]];
};
});
if (uncategorizedTagsSet.size > 0) {
@ -92,16 +104,16 @@ if (uncategorizedTagsSet.size > 0) {
</ul>
</section>
{
categorizedTags.map(([category, categorySlug, tagList]) =>
categorizedTags.map(({ name: category, id: categoryId, tags: tagList }) =>
tagList.length > 0 ? (
<section class="my-2" aria-labelledby={`category-${categorySlug}`}>
<h2 id={`category-${categorySlug}`} class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
<section class="my-2" aria-labelledby={`category-${categoryId}`}>
<h2 id={`category-${categoryId}`} 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(({ name, description }) => (
{tagList.map(({ id, name, description }) => (
<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(name)}`} title={description}>
<a class="hover:underline focus:underline" href={`/tags/${id}`} title={description}>
{name}
</a>
</li>

View file

@ -7,6 +7,7 @@ import { slug } from "github-slugger";
import GalleryLayout from "../../layouts/GalleryLayout.astro";
import Prose from "../../components/Prose.astro";
import { t } from "../../i18n";
import { DEFAULT_LANG } from "../../i18n";
type Props = {
tag: string;
@ -39,31 +40,31 @@ export const getStaticPaths: GetStaticPaths = async () => {
tags.add(tag);
});
});
const tagDescriptions = Object.fromEntries(
tagCategories.flatMap((category) =>
category.data.tags.reduce(
(acc, { name, description, related }) => {
if (related) {
related = related.filter((relatedTag) => {
if (relatedTag == name) {
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
return false;
}
if (!tags.has(relatedTag)) {
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
return false;
}
return true;
});
}
acc.push(
typeof name === "string" ? [name, { description, related }] : [name["eng"]!, { description, related }],
);
return acc;
},
[] as [string, { description?: string; related?: string[] }][],
),
),
const tagDescriptions = tagCategories.reduce(
(acc, category) => {
category.data.tags.forEach(({ name, description, related }) => {
if (related) {
related = related.filter((relatedTag) => {
if (relatedTag == name) {
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
return false;
}
if (!tags.has(relatedTag)) {
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
return false;
}
return true;
});
}
const key = typeof name === "string" ? name : name["eng"];
if (key in acc) {
throw new Error(`Duplicated tag "${key}" found in multiple tag-categories lists`);
}
acc[key] = { description, related };
});
return acc;
},
{} as Record<string, { description?: string; related?: string[] }>,
);
return [...tags]
.filter((tag) => !seriesTags.has(tag))
@ -71,8 +72,8 @@ export const getStaticPaths: GetStaticPaths = async () => {
params: { slug: slug(tag) } satisfies Params,
props: {
tag,
description: tagDescriptions[tag].description,
related: tagDescriptions[tag].related,
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()),
@ -85,23 +86,9 @@ export const getStaticPaths: GetStaticPaths = async () => {
const { tag, description, stories, games, related } = Astro.props;
if (!description) {
console.log(`Tag "${tag}" has no description`);
}
const count = stories.length + games.length;
let totalWorksWithTag: string = "";
if (count == 1) {
if (stories.length == 1) {
totalWorksWithTag = `One story tagged with "${tag}".`;
} else if (games.length == 1) {
totalWorksWithTag = `One game tagged with "${tag}".`;
}
} else if (stories.length == 0) {
totalWorksWithTag = `${games.length} games tagged with "${tag}".`;
} else if (games.length == 0) {
totalWorksWithTag = `${stories.length} stories tagged with "${tag}".`;
} else {
totalWorksWithTag = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`;
console.warn(`Tag "${tag}" has no description`);
}
const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stories.length, games.length);
---
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
@ -132,12 +119,7 @@ if (count == 1) {
<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.trim())}
>
{story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center">