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:
parent
579e5879e1
commit
17ef8c652c
34 changed files with 223 additions and 221 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue