Improvements to types and age verification screen

This commit is contained in:
Bad Manners 2024-03-24 14:22:39 -03:00
parent 18e98cdb3b
commit 7f7a62a391
78 changed files with 1132 additions and 1102 deletions

View file

@ -1,55 +0,0 @@
---
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="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>
</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">
Age verification
</div>
<div class="w-full max-w-xl">
<div
class="mx-6 mb-4 border-b border-stone-300 pb-4 text-base text-stone-700 sm:text-xl dark:border-stone-300 dark:text-stone-50"
>
You must be 18+ to access this page.
</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.
</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">
<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"
>
Cancel
</button>
<button
id="age-verification-accept"
class="rounded bg-bm-500 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-bm-400 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50"
>
I'm at least 18 years old
</button>
</div>
</div>
</div>
<script is:inline>
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 = decodeURIComponent(params.get("redirect") || "/");
});
</script>
</BaseLayout>

View file

@ -108,70 +108,67 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
}
function isPreferredWebsite(
user: CollectionEntry<"users">,
website: Website,
preferredChoices: readonly Website[],
): boolean {
const { preferredLink, links } = user.data;
if (!(website in links)) {
return false;
}
if (!preferredLink || preferredLink == website) {
return true;
}
return !preferredChoices.includes(preferredLink);
}
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite): string {
if (user.data.isAnonymous) {
return "anonymous";
}
switch (website) {
case "eka":
if (user.data.links.eka) {
if ("eka" in user.data.links) {
return `:icon${getUsernameForWebsite(user, "eka")}:`;
}
break;
case "furaffinity":
if (user.data.links.furaffinity) {
if ("furaffinity" in user.data.links) {
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
}
break;
case "weasyl":
if (user.data.links.weasyl) {
const weasylPreferredWebsites = ["furaffinity", "inkbunny", "sofurry"] as const;
if ("weasyl" in user.data.links) {
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
} else if (
user.data.links.furaffinity &&
!(["inkbunny", "sofurry"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "furaffinity", weasylPreferredWebsites)) {
return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`;
} else if (
user.data.links.inkbunny &&
!(["furaffinity", "sofurry"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "inkbunny", weasylPreferredWebsites)) {
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
} else if (
user.data.links.sofurry &&
!(["furaffinity", "inkbunny"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "sofurry", weasylPreferredWebsites)) {
return `<sf:${getUsernameForWebsite(user, "sofurry")}>`;
}
break;
case "inkbunny":
if (user.data.links.inkbunny) {
const inkbunnyPreferredWebsites = ["furaffinity", "sofurry", "weasyl"] as const;
if ("inkbunny" in user.data.links) {
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
} else if (
user.data.links.furaffinity &&
!(["sofurry", "weasyl"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "furaffinity", inkbunnyPreferredWebsites)) {
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
} else if (
user.data.links.sofurry &&
!(["furaffinity", "weasyl"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "sofurry", inkbunnyPreferredWebsites)) {
return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`;
} else if (
user.data.links.weasyl &&
!(["furaffinity", "sofurry"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "weasyl", inkbunnyPreferredWebsites)) {
return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`;
}
break;
case "sofurry":
if (user.data.links.sofurry) {
const sofurryPreferredWebsites = ["furaffinity", "inkbunny"] as const;
if ("sofurry" in user.data.links) {
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
} else if (
user.data.links.furaffinity &&
!(["inkbunny"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "furaffinity", sofurryPreferredWebsites)) {
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
} else if (
user.data.links.inkbunny &&
!(["furaffinity"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
} else if (isPreferredWebsite(user, "inkbunny", sofurryPreferredWebsites)) {
return `ib!${getUsernameForWebsite(user, "inkbunny")}`;
}
break;
@ -186,7 +183,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite):
throw new Error(`No preferredLink "${user.data.preferredLink}" for user ${user.id}`);
}
}
throw new Error(`No "${website}"-supported link for user "${user.id}" without preferredLink`);
throw new Error(`No "${website}" link for user "${user.id}" (consider setting preferredLink)`);
}
type Props = {
@ -268,12 +265,15 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, params: {
(_, group1, group2) => `[${group1}](${new URL(group2, new URL(`/stories/${story.slug}`, site)).toString()})`,
);
const headers = { "Content-Type": "text/markdown; charset=utf-8" };
const bbcodeExports: ReadonlyArray<ExportWebsite> = ["eka", "furaffinity", "inkbunny", "sofurry"] as const;
const markdownExport: ReadonlyArray<ExportWebsite> = ["weasyl"] as const;
// BBCode exports
if ((["eka", "furaffinity", "inkbunny", "sofurry"] satisfies ExportWebsite[] as ExportWebsite[]).includes(website)) {
if (bbcodeExports.includes(website)) {
storyDescription = he.decode(await marked.use({ renderer: bbcodeRenderer }).parse(storyDescription));
headers["Content-Type"] = "text/plain; charset=utf-8";
// Markdown exports (no-op)
} else if (!(["weasyl"] satisfies ExportWebsite[] as ExportWebsite[]).includes(website)) {
} else if (!markdownExport.includes(website)) {
console.log(`Unrecognized ExportWebsite "${website}"`);
return new Response(null, { status: 404 });
}
return new Response(`${storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()}\n`, { headers });

View file

@ -1,14 +1,12 @@
---
import { getCollection } from "astro:content";
import { getCollection, getEntry } from "astro:content";
import { Image } from "astro:assets";
import { getUnixTime } from "date-fns";
import GalleryLayout from "../../layouts/GalleryLayout.astro";
import mapImage from "../../assets/images/tlotm_map.jpg";
const stories = await getCollection(
"stories",
(story) => !story.data.isDraft && story.slug.startsWith("the-lost-of-the-marshes/"),
);
const series = await getEntry("series", "the-lost-of-the-marshes");
const stories = await getCollection("stories", (story) => !story.data.isDraft && story.data.series?.id === series.id);
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));
@ -18,8 +16,8 @@ const bonusChapters = stories
const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summary);
---
<GalleryLayout pageTitle="The Lost of the Marshes">
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">The Lost of the Marshes</h1>
<GalleryLayout pageTitle={series.data.name}>
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
<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>

View file

@ -10,83 +10,73 @@ const [stories, games, tagCategories] = await Promise.all([
]);
const tagsSet = new Set<string>();
const draftOnlyTagsSet = new Set<string>();
const seriesList: Record<string, string> = {};
stories.filter((story) => !story.data.isDraft).forEach((story) => {
story.data.tags.forEach((tag) => {
tagsSet.add(tag);
const seriesCollection = await getCollection("series");
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}"`,
);
games
.filter((game) => !game.data.isDraft)
.forEach((game) => {
game.data.tags.forEach((tag) => {
tagsSet.add(tag);
});
});
stories
.filter((story) => story.data.isDraft)
.forEach((story) => {
story.data.tags.forEach((tag) => {
if (!tagsSet.has(tag)) {
draftOnlyTagsSet.add(tag);
}
} 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) => {
if (!tagsSet.has(tag)) {
draftOnlyTagsSet.add(tag);
}
} else {
seriesList[series] = url;
}
}
});
stories.filter((story) => story.data.isDraft).forEach((story) => {
story.data.tags.forEach((tag) => {
if (!tagsSet.has(tag)) {
draftOnlyTagsSet.add(tag);
}
});
});
});
games.filter((game) => game.data.isDraft).forEach((game) => {
game.data.tags.forEach((tag) => {
if (!tagsSet.has(tag)) {
draftOnlyTagsSet.add(tag);
}
});
});
const uncategorizedTagsSet = new Set(tagsSet);
const categorizedTags: Array<[string, string, string[]]> = tagCategories.sort((a, b) => a.data.index - b.data.index).map(category => {
const tagList = category.data.tags.map(tag => {
if (typeof tag === "string") {
return tag
}
if (!("eng" in tag)) {
throw new Error(`"{[lang]: text}" tag must have an "eng" key: ${tag}`)
}
return tag["eng"]!
})
tagList.forEach((tag, index) => {
if (index !== tagList.findLastIndex((val) => tag == val)) {
throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories`)
}
})
return [category.data.name, category.id, tagList.filter(tag => {
if (draftOnlyTagsSet.has(tag)) {
console.log(`Omitting draft-only tag "${tag}"`);
return false;
}
if (tagsSet.has(tag)) {
uncategorizedTagsSet.delete(tag);
}
return true;
})];
})
const categorizedTags: Array<[string, string, string[]]> = tagCategories
.sort((a, b) => a.data.index - b.data.index)
.map((category) => {
const tagList = category.data.tags.map((tag) => {
if (typeof tag === "string") {
return tag;
}
if (!("eng" in tag)) {
throw new Error(`"{[lang]: text}" tag must have an "eng" key: ${tag}`);
}
return tag["eng"]!;
});
tagList.forEach((tag, index) => {
if (index !== tagList.findLastIndex((val) => tag == val)) {
throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories`);
}
});
return [
category.data.name,
category.id,
tagList.filter((tag) => {
if (draftOnlyTagsSet.has(tag)) {
console.log(`Omitting draft-only tag "${tag}"`);
return false;
}
if (tagsSet.has(tag)) {
uncategorizedTagsSet.delete(tag);
}
return true;
}),
];
});
if (uncategorizedTagsSet.size > 0) {
const tagList = [...uncategorizedTagsSet];
@ -102,10 +92,10 @@ if (uncategorizedTagsSet.size > 0) {
<h2 id="category-series" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Series</h2>
<ul class="list-disc pl-8">
{
Object.entries(seriesList).map(([series, url]) => (
seriesCollection.map((series) => (
<li>
<a class="text-link underline" href={url}>
{series}
<a class="text-link underline" href={series.data.url}>
{series.data.name}
</a>
</li>
))