This commit is contained in:
DrMint 2024-03-02 14:08:17 +01:00
parent 88b9613abf
commit f4cede5240
67 changed files with 1616 additions and 1007 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/*

View File

@ -20,6 +20,7 @@ export default defineConfig({
}, },
}), }),
], ],
devToolbar: { enabled: false },
server: { server: {
port: 12499, port: 12499,
host: true, host: true,

BIN
bun.lockb

Binary file not shown.

View File

@ -10,15 +10,20 @@
"astro": "astro", "astro": "astro",
"upgrade": "ncu", "upgrade": "ncu",
"script:download-payload-sdk": "bun run scripts/download-payload-sdk.ts", "script:download-payload-sdk": "bun run scripts/download-payload-sdk.ts",
"script:download-currencies": "bun run scripts/download-currencies.ts" "script:download-currencies": "bun run scripts/download-currencies.ts",
"script:download-wording-keys": "bun run scripts/download-wording-keys.ts"
},
"engines": {
"npm": ">=10.0.0",
"node": ">=19.7.0"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.5.4", "@astrojs/check": "^0.5.6",
"@astrojs/node": "^8.2.0", "@astrojs/node": "^8.2.1",
"@fontsource-variable/murecho": "^5.0.17", "@fontsource-variable/murecho": "^5.0.17",
"@fontsource-variable/vollkorn": "^5.0.19", "@fontsource-variable/vollkorn": "^5.0.19",
"accept-language": "^3.0.18", "accept-language": "^3.0.18",
"astro": "4.3.7", "astro": "4.4.6",
"astro-icon": "^1.1.0", "astro-icon": "^1.1.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
@ -26,13 +31,13 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/material-symbols": "^1.1.72", "@iconify-json/material-symbols": "^1.1.73",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"astro-meta-tags": "^0.2.1", "astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"bun-types": "^1.0.26", "bun-types": "^1.0.29",
"npm-check-updates": "^16.14.15", "npm-check-updates": "^16.14.15",
"postcss-preset-env": "^9.3.0", "postcss-preset-env": "^9.4.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }

View File

@ -1,16 +1,23 @@
--- ---
import Html from "./components/Html.astro"; import Html from "./components/Html.astro";
import Topbar from "./components/Topbar.astro"; import Topbar from "./components/Topbar/Topbar.astro";
import Footer from "./components/Footer.astro"; import Footer from "./components/Footer.astro";
import type { ComponentProps } from "astro/types"; import type { ParentPage } from "src/shared/payload/payload-sdk";
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
interface Props { interface Props {
parentPages?: ComponentProps<typeof Topbar>["parentPages"]; parentPages?: ParentPage[];
metaTitle?: string; metaTitle?: string;
hideFooterLinks?: boolean; hideFooterLinks?: boolean;
backgroundIllustration?: string | undefined;
} }
const { metaTitle, hideFooterLinks = false, parentPages } = Astro.props; const {
metaTitle,
hideFooterLinks = false,
parentPages,
backgroundIllustration,
} = Astro.props;
--- ---
{ {
@ -19,6 +26,11 @@ const { metaTitle, hideFooterLinks = false, parentPages } = Astro.props;
<Html title={metaTitle}> <Html title={metaTitle}>
<header> <header>
{
backgroundIllustration && (
<AppLayoutBackgroundImg src={backgroundIllustration} />
)
}
<Topbar parentPages={parentPages} /> <Topbar parentPages={parentPages} />
</header> </header>
<main><slot /></main> <main><slot /></main>

View File

@ -1,7 +1,7 @@
--- ---
import Html from "./components/Html.astro"; import Html from "./components/Html.astro";
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro"; import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
import Topbar from "./components/Topbar.astro"; import Topbar from "./components/Topbar/Topbar.astro";
import Footer from "./components/Footer.astro"; import Footer from "./components/Footer.astro";
import AppLayoutTitle from "./components/AppLayoutTitle.astro"; import AppLayoutTitle from "./components/AppLayoutTitle.astro";
import type { ComponentProps } from "astro/types"; import type { ComponentProps } from "astro/types";

View File

@ -25,7 +25,6 @@ const styleNoScript = `
} }
<img id={uniqueId} src={src} alt={alt} class="when-no-print" /> <img id={uniqueId} src={src} alt={alt} class="when-no-print" />
<noscript set:html={styleNoScript} /> <noscript set:html={styleNoScript} />
{ {
@ -36,7 +35,6 @@ const styleNoScript = `
img { img {
opacity: 0; opacity: 0;
transition: 3s opacity; transition: 3s opacity;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -54,6 +52,10 @@ const styleNoScript = `
} }
</style> </style>
{
/* ------------------------------------------- JS --------------------------------------------- */
}
<script define:vars={{ uniqueId }}> <script define:vars={{ uniqueId }}>
const element = document.getElementById(uniqueId); const element = document.getElementById(uniqueId);

View File

@ -8,12 +8,20 @@ interface Props {
const { title, subtitle, pretitle } = Astro.props; const { title, subtitle, pretitle } = Astro.props;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<h1 class="high-contrast-text"> <h1 class="high-contrast-text">
{pretitle && <span id="pretitle">{pretitle}&nbsp;</span>} {pretitle && <span id="pretitle">{pretitle}&nbsp;</span>}
<span id="title">{title}&nbsp;</span> <span id="title">{title}&nbsp;</span>
{subtitle && <span id="subtitle">{subtitle}</span>} {subtitle && <span id="subtitle">{subtitle}</span>}
</h1> </h1>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
h1 { h1 {
line-height: 0.8; line-height: 0.8;

View File

@ -131,6 +131,10 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
<div id="copyright" set:html={t("footer.disclaimer")} /> <div id="copyright" set:html={t("footer.disclaimer")} />
</footer> </footer>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
footer { footer {
border-top: 0.1em solid var(--color-base-1000); border-top: 0.1em solid var(--color-base-1000);
@ -251,6 +255,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
& > #license-section { & > #license-section {
grid-area: license; grid-area: license;
line-height: 1.2;
& > #common-creative { & > #common-creative {
display: flex; display: flex;
@ -288,6 +293,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
@media (max-width: 35rem) { @media (max-width: 35rem) {
place-content: center; place-content: center;
flex-wrap: wrap;
gap: clamp(24px, 8vw, 48px); gap: clamp(24px, 8vw, 48px);
& > a > svg { & > a > svg {
@ -301,6 +307,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
& > #copyright { & > #copyright {
border-left: 0.1em solid var(--color-base-1000); border-left: 0.1em solid var(--color-base-1000);
padding-left: 1em; padding-left: 1em;
line-height: 1.2;
@media (max-width: 35rem) { @media (max-width: 35rem) {
border: none; border: none;

View File

@ -16,7 +16,8 @@ const isIOS = parser.getOS().name === "iOS";
const { currentTheme } = Astro.locals; const { currentTheme } = Astro.locals;
/* -------------------------------------------- HTML -------------------------------------------- */ /* Keep that separator here or else it breaks the HTML
----------------------------------------------- HTML -------------------------------------------- */
--- ---
<html <html
@ -46,8 +47,14 @@ const { currentTheme } = Astro.locals;
/> />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<style is:global>
.when-no-js {
display: none;
}
</style>
<noscript> <noscript>
<style> <style is:global>
.when-js { .when-js {
display: none !important; display: none !important;
} }
@ -69,12 +76,6 @@ const { currentTheme } = Astro.locals;
} }
<style is:global> <style is:global>
@media print {
.when-no-print {
display: none !important;
}
}
html { html {
--color-base-0: #ffffff; --color-base-0: #ffffff;
--color-base-50: #fffaf3; --color-base-50: #fffaf3;
@ -107,6 +108,8 @@ const { currentTheme } = Astro.locals;
--color-shadow-1: var(--color-base-350); --color-shadow-1: var(--color-base-350);
--color-shadow-2: var(--color-base-300); --color-shadow-2: var(--color-base-300);
--color-critical-error: #940000;
--texture-dots: url(/img/paper-dots.webp); --texture-dots: url(/img/paper-dots.webp);
--texture-dots-blend: multiply; --texture-dots-blend: multiply;
@ -157,6 +160,8 @@ const { currentTheme } = Astro.locals;
--color-shadow-1: var(--color-base-0); --color-shadow-1: var(--color-base-0);
--color-shadow-2: var(--color-base-50); --color-shadow-2: var(--color-base-50);
--color-critical-error: red;
--texture-dots: url(/img/paper-dots-dark.webp); --texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay; --texture-dots-blend: overlay;
@ -170,57 +175,6 @@ const { currentTheme } = Astro.locals;
} }
&:not(.manual-theme) { &:not(.manual-theme) {
/* Get in between colors with https://colorkit.io/ */
@media (prefers-color-scheme: light) {
--color-base-0: #ffffff;
--color-base-50: #fffaf3;
--color-base-100: #fff4e6;
--color-base-125: #fef0dd;
--color-base-150: #fdebd4;
--color-base-200: #f7ddc2;
--color-base-250: #efcfb0;
--color-base-300: #e5be9e;
--color-base-350: #ddb08e;
--color-base-400: #d3a07c;
--color-base-450: #ca926c;
--color-base-500: #c0835d;
--color-base-550: #b3754f;
--color-base-600: #a26a47;
--color-base-650: #905e3f;
--color-base-700: #805438;
--color-base-750: #6e4a31;
--color-base-800: #5e402b;
--color-base-850: #4d3625;
--color-base-900: #3c2d1e;
--color-base-950: #2f2419;
--color-base-1000: #1f1a13;
--color-elevation-2: var(--color-base-100);
--color-elevation-1: var(--color-base-125);
--color-elevation-0: var(--color-base-150);
--color-shadow: var(--color-base-500);
--color-shadow-1: var(--color-base-350);
--color-shadow-2: var(--color-base-300);
--texture-dots: url(/img/paper-dots.webp);
--texture-dots-blend: multiply;
& .when-light-theme {
display: initial !important;
}
& .when-dark-theme {
display: none !important;
}
font-weight: 430;
strong {
font-weight: 600;
}
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
--color-base-1000: #ebeae7; --color-base-1000: #ebeae7;
--color-base-950: #eae5e0; --color-base-950: #eae5e0;
@ -253,6 +207,8 @@ const { currentTheme } = Astro.locals;
--color-shadow-1: var(--color-base-0); --color-shadow-1: var(--color-base-0);
--color-shadow-2: var(--color-base-50); --color-shadow-2: var(--color-base-50);
--color-critical-error: red;
--texture-dots: url(/img/paper-dots-dark.webp); --texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay; --texture-dots-blend: overlay;
@ -341,8 +297,21 @@ const { currentTheme } = Astro.locals;
p { p {
& a { & a {
transition-duration: 150ms;
transition-property: text-decoration-color, color;
color: var(--color-base-750); color: var(--color-base-750);
text-decoration: underline dotted 0.1em var(--color-base-650); text-decoration: underline dotted 0.1em var(--color-base-650);
&:hover {
color: var(--color-base-850);
text-decoration-color: var(--color-base-750);
}
&:active {
color: var(--color-base-1000);
text-decoration-color: var(--color-base-1000);
}
} }
} }
@ -369,6 +338,29 @@ const { currentTheme } = Astro.locals;
} }
} }
.pressable-label {
text-decoration: none;
flex-shrink: 0;
display: flex;
place-items: center;
gap: 0.4em;
padding: 0.7em 0.8em;
border-radius: 9999px;
cursor: pointer;
backdrop-filter: blur(10px);
transition: 150ms background-color;
&:hover {
background-color: var(--color-base-250);
}
&:active {
background-color: var(--color-base-300);
}
}
.pressable { .pressable {
--foreground-color: var(--color-base-650); --foreground-color: var(--color-base-650);
color: var(--foreground-color); color: var(--foreground-color);
@ -396,17 +388,13 @@ const { currentTheme } = Astro.locals;
} }
.hide-scrollbar { .hide-scrollbar {
scrollbar-width: none; /* Firefox */ scrollbar-width: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
} }
.when-no-js {
display: none;
}
.prose { .prose {
font-size: 16px; font-size: 16px;
line-height: 1.75; line-height: 1.75;
@ -425,6 +413,12 @@ const { currentTheme } = Astro.locals;
> p { > p {
margin-top: 1.25em; margin-top: 1.25em;
margin-bottom: 1.25em; margin-bottom: 1.25em;
> kbd {
background-color: var(--color-shadow-2);
padding: 0.15em 0.3em;
border-radius: 0.3em;
}
} }
> h2 { > h2 {
@ -443,6 +437,7 @@ const { currentTheme } = Astro.locals;
margin-top: 2em; margin-top: 2em;
margin-bottom: 1em; margin-bottom: 1em;
line-height: 1; line-height: 1;
scroll-margin: 1em;
} }
> h2 + h3, > h2 + h3,
@ -452,4 +447,16 @@ const { currentTheme } = Astro.locals;
margin-top: -0.75em; margin-top: -0.75em;
} }
} }
* {
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: var(--color-base-650) transparent;
}
@media print {
.when-no-print {
display: none !important;
}
}
</style> </style>

View File

@ -1,14 +1,15 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import ThemeSelector from "components/AppLayout/components/ThemeSelector.astro"; import ThemeSelector from "./components/ThemeSelector.astro";
import LanguageSelector from "components/AppLayout/components/LanguageSelector.astro"; import LanguageSelector from "./components/LanguageSelector.astro";
import CurrencySelector from "components/AppLayout/components/CurrencySelector.astro"; import CurrencySelector from "./components/CurrencySelector.astro";
import { getI18n } from "translations/translations"; import { getI18n } from "translations/translations";
import Tooltip from "components/Tooltip.astro"; import type { ParentPage } from "src/shared/payload/payload-sdk";
import ParentPagesButton from "./components/ParentPagesButton.astro";
interface Props { interface Props {
parentPages?: { name: string; slug: string; type: string }[] | undefined; parentPages?: ParentPage[] | undefined;
hideHomeButton?: boolean; hideHomeButton?: boolean;
} }
@ -24,31 +25,20 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<nav id="topbar" class="when-no-print"> <nav id="topbar" class="when-no-print">
{ {
(!hideHomeButton || parentPages.length > 0) && ( (!hideHomeButton || parentPages.length > 0) && (
<div id="breadcrumb" class="hide-scrollbar high-contrast-text"> <div id="left" class="hide-scrollbar high-contrast-text">
<a href="/"> <a href="/" class="pressable-label">
<Icon name="material-symbols:home" width={16} height={16} /> <Icon name="material-symbols:home" width={16} height={16} />
<p>{t("home.title")}</p> <p>{t("home.title")}</p>
</a> </a>
{parentPages.length > 0 && ( {parentPages.length > 0 && (
<Tooltip trigger="click"> <ParentPagesButton parentPages={parentPages} />
<div slot="tooltip-content">
<p>This content is part of these pages:</p>
<p>NieR / Concert</p>
<p>NieR:Automata / Concert</p>
<p>NieR:Theatrical Orchestra Concert 12020 Bluray</p>
</div>
<div>
<Icon name="material-symbols:keyboard-return" />
<p>4 parent pages</p>
</div>
</Tooltip>
)} )}
</div> </div>
) )
} }
<div id="toolbar"> <div id="toolbar" class="hide-scrollbar">
<a href={getLocalizedUrl("/search")}> <a href={getLocalizedUrl("/search")}>
<Button <Button
icon="material-symbols:search" icon="material-symbols:search"
@ -89,40 +79,16 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
flex-wrap: wrap-reverse; flex-wrap: wrap-reverse;
gap: 32px 64px; gap: 32px 64px;
& > #breadcrumb { & > #left {
display: flex; display: flex;
place-items: center; place-items: center;
overflow-x: scroll; overflow-x: scroll;
gap: 8px; gap: 8px;
margin-left: -0.8em; margin-left: -0.8em;
& > svg { & > :global(*) {
flex-shrink: 0; flex-shrink: 0;
} }
& > a,
& > :global(tippy-tooltip > div) {
text-decoration: none;
flex-shrink: 0;
display: flex;
place-items: center;
gap: 0.4em;
padding: 0.7em 0.8em;
border-radius: 9999px;
cursor: pointer;
backdrop-filter: blur(10px);
transition: 150ms background-color;
&:hover {
background-color: var(--color-base-250);
}
&:active {
background-color: var(--color-base-300);
}
}
} }
& > #toolbar { & > #toolbar {
@ -131,6 +97,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
gap: 12px; gap: 12px;
place-items: center; place-items: center;
justify-content: flex-end; justify-content: flex-end;
overflow-x: scroll;
@media (max-width: 28rem) { @media (max-width: 28rem) {
justify-content: center; justify-content: center;
@ -138,7 +105,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
@media (max-width: 22rem) { @media (max-width: 22rem) {
justify-content: space-between; justify-content: space-between;
gap: 0; gap: 12px;
} }
& > .separator { & > .separator {
@ -172,11 +139,3 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
} }
} }
</style> </style>
<script is:inline>
const breadcrumbElem = document.querySelector("nav#topbar > #breadcrumb");
breadcrumbElem?.scrollTo({
left: breadcrumbElem.scrollWidth,
behavior: "instant",
});
</script>

View File

@ -0,0 +1,52 @@
---
import { Collections, type ParentPage } from "src/shared/payload/payload-sdk";
import { getI18n } from "translations/translations";
interface Props {
parentPage: ParentPage;
}
const { parentPage } = Astro.props;
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
Astro.locals.currentLocale
);
const translation = getLocalizedMatch(parentPage.translations, {
name: parentPage.slug,
});
let href = "";
switch (parentPage.collection) {
case Collections.Folders:
href = getLocalizedUrl(`/folders/${parentPage.slug}`);
break;
default:
break;
}
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<a href={href}><span>{parentPage.tag}</span>{translation.name}</a>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
a {
display: flex;
place-items: center;
& > span {
background-color: var(--color-base-250);
border-radius: 9999px;
padding: 0.5em 0.6em;
margin-right: 0.5em;
font-size: 80%;
}
}
</style>

View File

@ -0,0 +1,51 @@
---
import Tooltip from "components/Tooltip.astro";
import type { ParentPage } from "src/shared/payload/payload-sdk";
import ParentPageLink from "./ParentPageLink.astro";
import { Icon } from "astro-icon/components";
import { getI18n } from "translations/translations";
interface Props {
parentPages: ParentPage[];
}
const { parentPages } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content">
<p>This content is part of these pages:</p>
{
parentPages.map((parentPage) => (
<ParentPageLink parentPage={parentPage} />
))
}
</div>
<div class="pressable-label">
<Icon name="material-symbols:keyboard-return" />
<p>
{
t("header.nav.parentPages.label", {
count: parentPages.length,
})
}
</p>
</div>
</Tooltip>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#tooltip-content {
display: grid;
gap: 1em;
}
</style>

View File

@ -0,0 +1,38 @@
---
import {
isBlockLineBlock,
type GenericBlock,
isBlockCueBlock,
isBlockSpacerBlock,
} from "src/shared/payload/payload-sdk";
import LineBlock from "./components/LineBlock.astro";
import CueBlock from "./components/CueBlock.astro";
import ErrorMessage from "components/ErrorMessage.astro";
import SpacerBlock from "./components/SpacerBlock.astro";
interface Props {
block: GenericBlock;
}
const { block } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
{
isBlockLineBlock(block) ? (
<LineBlock block={block} />
) : isBlockCueBlock(block) ? (
<CueBlock block={block} />
) : isBlockSpacerBlock(block) ? (
<SpacerBlock block={block} />
) : (
<ErrorMessage
title={`Unknown block type: ${block.blockType}`}
description="Please contact website technical administrator."
/>
)
}

View File

@ -0,0 +1,28 @@
---
import RichText from "components/RichText/RichText.astro";
import type { CueBlock } from "src/shared/payload/payload-sdk";
interface Props {
block: CueBlock;
}
const { block } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div>
<RichText content={block.content} />
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
div {
grid-column: span 2;
}
</style>

View File

@ -0,0 +1,38 @@
---
import RichText from "components/RichText/RichText.astro";
import type { LineBlock } from "src/shared/payload/payload-sdk";
interface Props {
block: LineBlock;
}
const { block } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div id="line">
<p>{block.blockName}</p>
<div>
<RichText content={block.content} />
</div>
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#line {
display: grid;
grid-column: span 2;
grid-template-columns: subgrid;
p {
color: var(--color-base-650);
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,18 @@
---
import type { SpacerBlock } from "src/shared/payload/payload-sdk";
interface Props {
block: SpacerBlock;
}
const { block } = Astro.props;
const spaceSizeToRem: Record<SpacerBlock["size"], number> = {
Small: 1,
Medium: 2,
Large: 4,
XLarge: 8,
};
---
<div style={`height: ${spaceSizeToRem[block.size]}rem`}></div>

View File

@ -1,62 +0,0 @@
---
---
<details>
<summary><slot name="header" /></summary>
<div class="content">
<div class="content2">
<slot />
</div>
</div>
</details>
<style>
details {
--foreground-color: var(--color-base-650);
color: var(--foreground-color);
border: 0.1em solid var(--foreground-color);
background-color: var(--color-elevation-0);
border-radius: 1.25em;
transition: all 1s;
&[open] {
& > summary {
border-bottom: 1px solid #aaa;
}
& > .content {
animation: animate 0.5s forwards;
}
}
& > summary {
list-style: none;
height: 2.5em;
padding-left: 1em;
padding-right: 1em;
display: flex;
place-items: center;
gap: 1em;
}
& > .content {
padding: 1em;
display: grid;
& > .content2 {
overflow: hidden;
}
}
}
@keyframes animate {
from {
grid-template-rows: 0fr;
}
to {
grid-template-rows: 1fr;
}
}
</style>

View File

@ -7,6 +7,10 @@ interface Props {
const { wrapper: Wrapper, condition } = Astro.props; const { wrapper: Wrapper, condition } = Astro.props;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
{ {
condition ? ( condition ? (
<Wrapper> <Wrapper>

View File

@ -0,0 +1,35 @@
---
import { getI18n } from "translations/translations";
import Metadata from "./Metadata.astro";
interface Props {
translators?: string[] | undefined;
transcribers?: string[] | undefined;
proofreaders?: string[] | undefined;
}
const { translators = [], transcribers = [], proofreaders = [] } = Astro.props;
const { formatRecorder } = await getI18n(Astro.locals.currentLocale);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Metadata
icon="material-symbols:person-outline"
title="Translators"
values={translators.map((id) => formatRecorder(id))}
/>
<Metadata
icon="material-symbols:person-edit-outline"
title="Transcribers"
values={transcribers.map((id) => formatRecorder(id))}
/>
<Metadata
icon="material-symbols:person-check-outline"
title="Proofreaders"
values={proofreaders.map((id) => formatRecorder(id))}
/>

View File

@ -0,0 +1,47 @@
---
import { Icon } from "astro-icon/components";
interface Props {
title: string;
description?: string;
}
const { title, description } = Astro.props;
---
<div>
<Icon name="material-symbols:error-outline" width={32} height={32} />
<p id="title">{title}</p>
{description && <p>{description}</p>}
</div>
<style>
@keyframes flashingRed {
from {
background-color: #ff000022;
}
to {
background-color: #ff000033;
}
}
div {
color: var(--color-critical-error) !important;
padding: 2em 2em !important;
margin-block: 4em !important;
border-radius: 1em;
display: grid;
place-items: center;
animation: flashingRed;
animation-duration: 0.5s;
animation-direction: alternate;
animation-iteration-count: infinite;
& > #title {
font-weight: 600;
font-size: 120%;
}
}
</style>

View File

@ -1,31 +0,0 @@
---
import { getRandomId } from "src/utils/random";
interface Props {
src: string;
alt?: string;
id?: string;
class?: string;
}
const { src, alt } = Astro.props;
const uniqueId = getRandomId();
---
<img id={uniqueId} src={src} alt={alt} />
<script define:vars={{ uniqueId }}>
const element = document.getElementById(uniqueId);
element.addEventListener("load", () => {
element.style.opacity = 1;
});
</script>
<style>
img {
opacity: 0;
transition: 3s opacity;
}
</style>

View File

@ -0,0 +1,66 @@
---
import MasoActor from "components/Maso/MasoActor.astro";
import Tooltip from "components/Tooltip.astro";
import Button from "components/Button.astro";
import { getI18n } from "translations/translations";
interface Props {
currentLang: string;
getPartialUrl: (locale: string) => string;
availableLanguages: string[];
}
const { currentLang, getPartialUrl, availableLanguages } = Astro.props;
const { formatLocale } = await getI18n(Astro.locals.currentLocale);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div id="lang-selector" class="when-js when-no-print">
<Tooltip trigger="click">
<Button
icon="material-symbols:translate"
title={currentLang.toUpperCase()}
/>
<div id="tooltip-content" slot="tooltip-content">
{
availableLanguages.map((id) => (
<MasoActor
class:list={{ current: id === currentLang }}
href={getPartialUrl(id)}
>
{formatLocale(id)}
</MasoActor>
))
}
</div>
</Tooltip>
<p class="high-contrast-text">
This content is available is {availableLanguages.length} languages.
</p>
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#lang-selector {
display: flex;
align-items: center;
gap: 1em;
#tooltip-content {
display: grid;
gap: 0.5em;
& > .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
}
</style>

View File

@ -11,6 +11,6 @@
} }
<script> <script>
class MasoTarget extends HTMLElement {} import { customElement } from "src/utils/customElements";
customElements.define("maso-target", MasoTarget); customElement("maso-target");
</script> </script>

View File

@ -12,6 +12,10 @@ const { icon, title, values } = Astro.props;
if (values.length === 0) return; if (values.length === 0) return;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div id="container"> <div id="container">
<div id="title"> <div id="title">
<Icon name={icon} width={24} height={24} /> <Icon name={icon} width={24} height={24} />
@ -22,6 +26,10 @@ if (values.length === 0) return;
</div> </div>
</div> </div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
#container { #container {
display: grid; display: grid;

View File

@ -2,41 +2,34 @@
import type { RichTextContext } from "src/utils/richText"; import type { RichTextContext } from "src/utils/richText";
import RTSection from "./components/RTSection.astro"; import RTSection from "./components/RTSection.astro";
import RTTranscript from "./components/RTTranscript.astro"; import RTTranscript from "./components/RTTranscript.astro";
import {
isBlockNodeSectionBlock,
isBlockNodeSpacerBlock,
isBlockNodeTranscriptBlock,
type RichTextBlockNode,
} from "src/shared/payload/payload-sdk";
import ErrorMessage from "components/ErrorMessage.astro";
import RTSpacer from "./components/RTSpacer.astro";
interface Props { interface Props {
node: { node: RichTextBlockNode;
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
};
};
context: RichTextContext; context: RichTextContext;
} }
const { node, context } = Astro.props; const { node, context } = Astro.props;
let NodeElement;
switch (node.fields.blockType) {
case "sectionBlock":
NodeElement = RTSection;
break;
case "transcriptBlock":
NodeElement = RTTranscript;
break;
}
--- ---
{ {
NodeElement ? ( isBlockNodeSectionBlock(node) ? (
<NodeElement node={node} context={context} /> <RTSection node={node} context={context} />
) : isBlockNodeTranscriptBlock(node) ? (
<RTTranscript node={node} context={context} />
) :isBlockNodeSpacerBlock(node) ? (
<RTSpacer node={node} context={context} />
) : ( ) : (
<p>{`Unknown block type: ${node.fields.blockType}. Please contact website technical administrator.`}</p> <ErrorMessage
title={`Unknown block type: ${node.fields.blockType}`}
description="Please contact website technical administrator."
/>
) )
} }

View File

@ -1,27 +0,0 @@
---
import RichText from "components/RichText/RichText.astro";
import type { RichTextContent } from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText";
interface Props {
block: {
id: string;
content: RichTextContent;
blockType: string;
blockName: string;
};
context: RichTextContext;
}
const { block, context } = Astro.props;
---
<div>
<RichText content={block.content} context={context} />
</div>
<style>
div {
grid-column: span 2;
}
</style>

View File

@ -1,41 +0,0 @@
---
import RichText from "components/RichText/RichText.astro";
import type { RichTextContent } from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText";
interface Props {
block: {
id: string;
content: RichTextContent;
blockType: string;
blockName: string;
};
context: RichTextContext;
}
const { block, context } = Astro.props;
---
<p>{block.blockName}</p>
<div>
<RichText content={block.content} context={context} />
</div>
<style>
p {
color: var(--color-base-650);
font-weight: 500;
}
@media (max-width: 35rem) {
p {
grid-column: 1;
margin-bottom: -1em;
}
div {
grid-column: 1;
margin-bottom: 1em;
}
}
</style>

View File

@ -1,22 +1,12 @@
--- ---
import RichText from "components/RichText/RichText.astro"; import RichText from "components/RichText/RichText.astro";
import type { RichTextContent } from "src/shared/payload/payload-sdk"; import type {
RichTextSectionBlock,
} from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText"; import type { RichTextContext } from "src/utils/richText";
interface Props { interface Props {
node: { node: RichTextSectionBlock;
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
lines: RichTextContent;
};
};
context: RichTextContext; context: RichTextContext;
} }
@ -25,19 +15,43 @@ const { node, context } = Astro.props;
{ {
context.depth < 2 ? ( context.depth < 2 ? (
<h2>{node.fields.blockName}</h2> <h2 id={node.fields.anchorHash}>
<span>{`${node.fields.anchorHash} `}</span>
{node.fields.blockName}
</h2>
) : context.depth === 2 ? ( ) : context.depth === 2 ? (
<h3>{node.fields.blockName}</h3> <h3 id={node.fields.anchorHash}>
<span>{`${node.fields.anchorHash} `}</span>
{node.fields.blockName}
</h3>
) : context.depth === 3 ? ( ) : context.depth === 3 ? (
<h4>{node.fields.blockName}</h4> <h4 id={node.fields.anchorHash}>
<span>{`${node.fields.anchorHash} `}</span>
{node.fields.blockName}
</h4>
) : context.depth === 4 ? ( ) : context.depth === 4 ? (
<h5>{node.fields.blockName}</h5> <h5 id={node.fields.anchorHash}>
<span>{`${node.fields.anchorHash} `}</span>
{node.fields.blockName}
</h5>
) : ( ) : (
<h6>{node.fields.blockName}</h6> <h6 id={node.fields.anchorHash}>
<span>{`${node.fields.anchorHash} `}</span>
{node.fields.blockName}
</h6>
) )
} }
<RichText <RichText
content={node.fields.lines} content={node.fields.content}
context={{ ...context, depth: context.depth + 1 }} context={{ ...context, depth: context.depth + 1 }}
/> />
<style>
span {
color: var(--color-base-650);
font-weight: 500;
font-size: 70%;
margin-right: 0.3em;
}
</style>

View File

@ -0,0 +1,14 @@
---
import type { RichTextContext } from "src/utils/richText";
import type { RichTextSpacerBlock } from "src/shared/payload/payload-sdk";
import SpacerBlock from "components/Blocks/components/SpacerBlock.astro";
interface Props {
node: RichTextSpacerBlock;
context: RichTextContext;
}
const { node } = Astro.props;
---
<SpacerBlock block={node.fields} />

View File

@ -1,52 +1,25 @@
--- ---
import type { RichTextContext } from "src/utils/richText"; import type { RichTextContext } from "src/utils/richText";
import RTLine from "./RTLine.astro"; import type { RichTextTranscriptBlock } from "src/shared/payload/payload-sdk";
import RTCue from "./RTCue.astro"; import Block from "components/Blocks/Block.astro";
interface Props { interface Props {
node: { node: RichTextTranscriptBlock;
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
lines: { blockType: string }[];
};
};
context: RichTextContext; context: RichTextContext;
} }
const { node, context } = Astro.props; const { node } = Astro.props;
--- ---
<div> <div>
{ {node.fields.lines.map((block) => <Block block={block} />)}
node.fields.lines.map((block) => {
switch (block.blockType) {
case "lineBlock":
return <RTLine block={block} context={context} />;
case "cueBlock":
return <RTCue block={block} context={context} />;
default:
return (
<p>{`Unknown block type: ${block.blockType}. Please contact website technical administrator.`}</p>
);
}
})
}
</div> </div>
<style> <style>
div { div {
padding-block: 1em; padding-block: 1em;
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: [name] auto [text] 1fr;
gap: 1.5em 2em; gap: 1.5em 2em;
} }
</style> </style>

View File

@ -3,24 +3,15 @@ import type { RichTextContext } from "src/utils/richText";
import RTNode from "../RTNode.astro"; import RTNode from "../RTNode.astro";
import RTCustomLink from "./components/RTCustomLink.astro"; import RTCustomLink from "./components/RTCustomLink.astro";
import RTInternalLink from "./components/RTInternalLink.astro"; import RTInternalLink from "./components/RTInternalLink.astro";
import {
isLinkNodeCustomLinkNode,
isLinkNodeInternalLinkNode,
type RichTextLinkNode,
} from "src/shared/payload/payload-sdk";
import ErrorMessage from "components/ErrorMessage.astro";
interface Props { interface Props {
node: { node: RichTextLinkNode;
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
version: number;
fields: {
linkType: "internal" | "custom";
doc: any;
url: string;
newTab: boolean;
};
[k: string]: unknown;
};
context: RichTextContext; context: RichTextContext;
} }
@ -28,19 +19,22 @@ const { node, context } = Astro.props;
--- ---
{ {
node.fields.linkType === "custom" ? ( isLinkNodeCustomLinkNode(node) ? (
<RTCustomLink href={node.fields.url} newTab={node.fields.newTab}> <RTCustomLink href={node.fields.url} newTab={node.fields.newTab}>
{node.children.map((node) => ( {node.children.map((node) => (
<RTNode node={node} context={context} /> <RTNode node={node} context={context} />
))} ))}
</RTCustomLink> </RTCustomLink>
) : node.fields.linkType === "internal" ? ( ) : isLinkNodeInternalLinkNode(node) ? (
<RTInternalLink doc={node.fields.doc}> <RTInternalLink doc={node.fields.doc}>
{node.children.map((node) => ( {node.children.map((node) => (
<RTNode node={node} context={context} /> <RTNode node={node} context={context} />
))} ))}
</RTInternalLink> </RTInternalLink>
) : ( ) : (
<p>{`Unknown link type: ${node.fields.linkType}. Please contact website technical administrator.`}</p> <ErrorMessage
title={`Unknown link type: ${node.fields.linkType}`}
description="Please contact website technical administrator."
/>
) )
} }

View File

@ -1,4 +1,5 @@
--- ---
import ErrorMessage from "components/ErrorMessage.astro";
import { getI18n } from "translations/translations"; import { getI18n } from "translations/translations";
interface Props { interface Props {
@ -18,6 +19,9 @@ const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<slot /> <slot />
</a> </a>
) : ( ) : (
<p>{`Unknown internal link: ${doc.relationTo}. Please contact website technical administrator.`}</p> <ErrorMessage
title={`Unknown internal link: ${doc.relationTo}`}
description="Please contact website technical administrator."
/>
) )
} }

View File

@ -2,24 +2,16 @@
import type { RichTextContext } from "src/utils/richText"; import type { RichTextContext } from "src/utils/richText";
import RTBasicListItem from "./components/RTBasicListItem.astro"; import RTBasicListItem from "./components/RTBasicListItem.astro";
import RTCheckListItem from "./components/RTCheckListItem.astro"; import RTCheckListItem from "./components/RTCheckListItem.astro";
import {
isListNodeBulletListNode,
isListNodeCheckListNode,
isListNodeNumberListNode,
type RichTextListNode,
} from "src/shared/payload/payload-sdk";
import ErrorMessage from "components/ErrorMessage.astro";
interface Props { interface Props {
node: { node: RichTextListNode;
type: string;
version: number;
listType: string;
children: {
type: string;
version: number;
[k: string]: unknown;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
}[];
[k: string]: unknown;
};
context: RichTextContext; context: RichTextContext;
} }
@ -27,27 +19,28 @@ const { node, context } = Astro.props;
--- ---
{ {
node.listType === "number" ? ( isListNodeNumberListNode(node) ? (
<ol> <ol>
{node.children.map((node) => ( {node.children.map((node) => (
<RTBasicListItem node={node} context={context} /> <RTBasicListItem node={node} context={context} />
))} ))}
</ol> </ol>
) : node.listType === "bullet" ? ( ) : isListNodeBulletListNode(node) ? (
<ul> <ul>
{node.children.map((node) => ( {node.children.map((node) => (
<RTBasicListItem node={node} context={context} /> <RTBasicListItem node={node} context={context} />
))} ))}
</ul> </ul>
) : node.listType === "check" ? ( ) : isListNodeCheckListNode(node) ? (
<ul> <ul>
{node.children.map((node) => ( {node.children.map((node) => (
<RTCheckListItem node={node} context={context} /> <RTCheckListItem node={node} context={context} />
))} ))}
</ul> </ul>
) : ( ) : (
<p> <ErrorMessage
{`Unknown list type: ${node.listType}. Please contact website technical administrator.`} title={`Unknown list link: ${node.listType}`}
</p> description="Please contact website technical administrator."
/>
) )
} }

View File

@ -5,46 +5,47 @@ import RTText from "./RTText/RTText.astro";
import RTLink from "./RTLink/RTLink.astro"; import RTLink from "./RTLink/RTLink.astro";
import RTBlock from "./RTBlock/RTBlock.astro"; import RTBlock from "./RTBlock/RTBlock.astro";
import type { RichTextContext } from "src/utils/richText"; import type { RichTextContext } from "src/utils/richText";
import {
isNodeBlockNode,
isNodeLinkNode,
isNodeListNode,
isNodeParagraphNode,
isNodeTabNode,
isNodeTextNode,
type RichTextNode,
} from "src/shared/payload/payload-sdk";
import RTTab from "./RTTab.astro";
import ErrorMessage from "components/ErrorMessage.astro";
interface Props { interface Props {
node: { node: RichTextNode;
type: string;
version: number;
[k: string]: unknown;
};
context: RichTextContext; context: RichTextContext;
} }
const { node, context } = Astro.props; const { node, context } = Astro.props;
let NodeElement;
switch (node.type) {
case "paragraph":
NodeElement = RTParagraph;
break;
case "list":
NodeElement = RTList;
break;
case "text":
NodeElement = RTText;
break;
case "link":
NodeElement = RTLink;
break;
case "block":
NodeElement = RTBlock;
break;
}
--- ---
{ {
NodeElement ? ( /* ------------------------------------------- HTML ------------------------------------------- */
<NodeElement node={node} context={context} /> }
{
isNodeParagraphNode(node) ? (
<RTParagraph node={node} context={context} />
) : isNodeListNode(node) ? (
<RTList node={node} context={context} />
) : isNodeTextNode(node) ? (
<RTText node={node} context={context} />
) : isNodeLinkNode(node) ? (
<RTLink node={node} context={context} />
) : isNodeBlockNode(node) ? (
<RTBlock node={node} context={context} />
) : isNodeTabNode(node) ? (
<RTTab />
) : ( ) : (
<p>{`Unknown node type: ${node.type}. Please contact website technical administrator.`}</p> <ErrorMessage
title={`Unknown node type: ${node.type}`}
description="Please contact website technical administrator."
/>
) )
} }

View File

@ -1,18 +1,10 @@
--- ---
import type { RichTextContext } from "src/utils/richText"; import type { RichTextContext } from "src/utils/richText";
import RTNode from "./RTNode.astro"; import RTNode from "./RTNode.astro";
import type { RichTextParagraphNode } from "src/shared/payload/payload-sdk";
interface Props { interface Props {
node: { node: RichTextParagraphNode;
type: string;
version: number;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
[k: string]: unknown;
};
context: RichTextContext; context: RichTextContext;
} }
@ -23,9 +15,12 @@ const { node, context } = Astro.props;
/* ------------------------------------------- HTML ------------------------------------------- */ /* ------------------------------------------- HTML ------------------------------------------- */
} }
<p>{node.children.map((node) => <RTNode node={node} context={context} />)}</p>
{ {
/* ------------------------------------------- CSS -------------------------------------------- */ node.children.length > 0 && (
<p style={`text-align: ${node.format};`}>
{node.children.map((node) => (
<RTNode node={node} context={context} />
))}
</p>
)
} }

View File

@ -0,0 +1,4 @@
---
---
{""}

View File

@ -7,15 +7,12 @@ import RTLineThrough from "./components/RTLineThrough.astro";
import RTSubscript from "./components/RTSubscript.astro"; import RTSubscript from "./components/RTSubscript.astro";
import RTSuperscript from "./components/RTSuperscript.astro"; import RTSuperscript from "./components/RTSuperscript.astro";
import RTInlineCode from "./components/RTInlineCode.astro"; import RTInlineCode from "./components/RTInlineCode.astro";
import type { RichTextContext } from "src/utils/richText";
import type { RichTextTextNode } from "src/shared/payload/payload-sdk";
interface Props { interface Props {
node: { node: RichTextTextNode;
type: string; context: RichTextContext;
version: number;
format: number;
text: string;
[k: string]: unknown;
};
} }
const { node } = Astro.props; const { node } = Astro.props;
@ -23,8 +20,14 @@ const { node } = Astro.props;
<ConditionalWrapper wrapper={RTBold} condition={Boolean(node.format & 1)}> <ConditionalWrapper wrapper={RTBold} condition={Boolean(node.format & 1)}>
<ConditionalWrapper wrapper={RTItalic} condition={Boolean(node.format & 2)}> <ConditionalWrapper wrapper={RTItalic} condition={Boolean(node.format & 2)}>
<ConditionalWrapper wrapper={RTLineThrough} condition={Boolean(node.format & 4)}> <ConditionalWrapper
<ConditionalWrapper wrapper={RTUnderline} condition={Boolean(node.format & 8)}> wrapper={RTLineThrough}
condition={Boolean(node.format & 4)}
>
<ConditionalWrapper
wrapper={RTUnderline}
condition={Boolean(node.format & 8)}
>
<ConditionalWrapper <ConditionalWrapper
wrapper={RTInlineCode} wrapper={RTInlineCode}
condition={Boolean(node.format & 16)} condition={Boolean(node.format & 16)}

View File

@ -0,0 +1,46 @@
---
import type { TableOfContentEntry } from "src/shared/payload/payload-sdk";
import TableOfContentItem from "./components/TableOfContentItem.astro";
import { Icon } from "astro-icon/components";
interface Props {
toc: TableOfContentEntry[];
}
const { toc } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div id="container">
<div id="title">
<Icon name="material-symbols:list-alt-outline" width={24} height={24} />
<p>Table of Content</p>
</div>
<ol>
{toc.map((entry) => <TableOfContentItem entry={entry} />)}
</ol>
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#container {
& > #title {
display: flex;
place-items: center;
gap: 8px;
margin-bottom: 0.75em;
& > p {
font-size: 1.5em;
font-weight: 600;
translate: 0px -0.1em;
}
}
}
</style>

View File

@ -0,0 +1,65 @@
---
import type { TableOfContentEntry } from "src/shared/payload/payload-sdk";
interface Props {
entry: TableOfContentEntry;
}
const { entry } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<li data-prefix={entry.prefix}>
<a href={`#${entry.prefix}`}>{entry.title}</a>
{
entry.children.length > 0 && (
<ol>
{entry.children.map((entry) => (
<Astro.self entry={entry} />
))}
</ol>
)
}
</li>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
a {
font-weight: 500;
text-decoration: underline dotted 0.1em;
text-decoration-color: transparent;
transition-duration: 150ms;
transition-property: text-decoration-color, color;
&:hover {
color: var(--color-base-750);
text-decoration-color: var(--color-base-650);
}
&:active {
color: var(--color-base-650);
text-decoration-color: var(--color-base-550);
}
}
li {
margin-block: 0.5em;
}
li {
&::marker {
content: attr(data-prefix) " ";
color: var(--color-base-650);
margin-right: 1em;
}
line-height: 125%;
}
</style>

View File

@ -0,0 +1,25 @@
---
import Metadata from "components/Metadata.astro";
import { getI18n } from "translations/translations";
interface Props {
slug: string;
icon: string;
values: string[];
}
const { icon, slug, values } = Astro.props;
const { formatTag, formatTagsGroup } = await getI18n(
Astro.locals.currentLocale
);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Metadata
icon={icon}
title={formatTagsGroup(slug)}
values={values.map(formatTag)}
/>

View File

@ -0,0 +1,32 @@
---
import TagGroup from "./TagGroup.astro";
interface Props {
tagGroups: { slug: string; icon: string; values: string[] }[];
}
const { tagGroups } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div>{tagGroups.map((tag) => <TagGroup {...tag} />)}</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
div {
@media (max-width: 35rem) {
margin-block: 5em;
gap: 2em;
}
margin-block: 2em;
display: grid;
gap: 1em;
}
</style>

View File

@ -38,7 +38,8 @@ const localeNegotiator = defineMiddleware(({ cookies, url, request }, next) => {
const currentLocale = getCurrentLocale(url.pathname); const currentLocale = getCurrentLocale(url.pathname);
const acceptedLocale = getBestAcceptedLanguage(request); const acceptedLocale = getBestAcceptedLanguage(request);
const cookieLocale = getCookieLocale(cookies); const cookieLocale = getCookieLocale(cookies);
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? defaultLocale; const bestMatchingLocale =
cookieLocale ?? acceptedLocale ?? currentLocale ?? defaultLocale;
if (!currentLocale) { if (!currentLocale) {
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname); const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);

View File

@ -8,6 +8,10 @@ interface Props {
const { img, name, href } = Astro.props; const { img, name, href } = Astro.props;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<a href={href} aria-label={name} class="pressable"> <a href={href} aria-label={name} class="pressable">
{ {
img ? ( img ? (
@ -16,11 +20,17 @@ const { img, name, href } = Astro.props;
<img src={img.dark} class="when-dark-theme" alt={name} title={name} /> <img src={img.dark} class="when-dark-theme" alt={name} title={name} />
</> </>
) : ( ) : (
<div><p>{name}</p></div> <div>
<p>{name}</p>
</div>
) )
} }
</a> </a>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
a { a {
font-size: 24px; font-size: 24px;
@ -46,6 +56,5 @@ const { img, name, href } = Astro.props;
display: grid; display: grid;
place-content: center; place-content: center;
} }
} }
</style> </style>

View File

@ -9,12 +9,20 @@ interface Props {
const { pretitle, subtitle, title, href } = Astro.props; const { pretitle, subtitle, title, href } = Astro.props;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<a href={href} class="pressable"> <a href={href} class="pressable">
<p class="pretitle">{pretitle}</p> <p class="pretitle">{pretitle}</p>
<h3>{title}</h3> <h3>{title}</h3>
<p>{subtitle}</p> <p>{subtitle}</p>
</a> </a>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
a { a {
display: flex; display: flex;

View File

@ -3,12 +3,16 @@ import { payload } from "src/shared/payload/payload-sdk";
import { getI18n } from "translations/translations"; import { getI18n } from "translations/translations";
import CategoryCard from "./CategoryCard.astro"; import CategoryCard from "./CategoryCard.astro";
const folders = await payload.getRootFolders() const folders = await payload.getRootFolders();
const { getLocalizedUrl, getLocalizedMatch } = await getI18n( const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
Astro.locals.currentLocale Astro.locals.currentLocale
); );
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
{ {
folders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => ( folders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => (
<CategoryCard <CategoryCard

View File

@ -10,6 +10,10 @@ interface Props {
const { icon, subtitle, title, href } = Astro.props; const { icon, subtitle, title, href } = Astro.props;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<a href={href} class="pressable"> <a href={href} class="pressable">
<Icon name={icon} /> <Icon name={icon} />
<div id="right"> <div id="right">
@ -18,6 +22,10 @@ const { icon, subtitle, title, href } = Astro.props;
</div> </div>
</a> </a>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
a { a {
display: flex; display: flex;

View File

@ -1,62 +0,0 @@
---
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import MasoActor from "components/Maso/MasoActor.astro";
import MasoTarget from "components/Maso/MasoTarget.astro";
import { getI18n } from "translations/translations";
export const partial = true;
interface Props {
lang?: string;
}
const reqUrl = new URL(Astro.request.url);
const lang = Astro.props.lang ?? reqUrl.searchParams.get("lang")!;
const { t } = await getI18n(lang);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<MasoTarget>
<Tooltip trigger="click" class="when-js">
<Button
icon="material-symbols:translate"
title={lang.toUpperCase()}
ariaLabel={t("header.topbar.language.tooltip")}
/>
<div id="content" slot="tooltip-content">
{
["en", "fr"].map((locale) => (
<MasoActor
class:list={{ current: locale === lang }}
href={`/api/content?lang=${locale}`}
>
{locale.toString().toUpperCase()}
</MasoActor>
))
}
</div>
</Tooltip>
<div set:html={t("home.description")} />
</MasoTarget>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#content {
display: grid;
gap: 0.5em;
& > :global(.current) {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
</style>

View File

@ -1,94 +0,0 @@
---
import MasoActor from "components/Maso/MasoActor.astro";
import Tooltip from "components/Tooltip.astro";
import Button from "components/Button.astro";
import { getI18n } from "translations/translations";
import Metadata from "pages/[locale]/api/contents/_components/Metadata.astro";
interface Props {
currentLang: string;
getPartialUrl: (locale: string) => string;
availableLanguages: string[];
translators?: string[] | undefined;
transcribers?: string[] | undefined;
proofreaders?: string[] | undefined;
}
const {
currentLang,
getPartialUrl,
availableLanguages,
translators = [],
transcribers = [],
proofreaders = [],
} = Astro.props;
const { formatLocale, formatRecorder } = await getI18n(
Astro.locals.currentLocale
);
---
{
availableLanguages.length > 1 && (
<div id="lang-selector" class="when-js when-no-print">
<Tooltip trigger="click">
<Button
icon="material-symbols:translate"
title={currentLang.toUpperCase()}
/>
<div id="tooltip-content" slot="tooltip-content">
{availableLanguages.map((id) => (
<MasoActor
class:list={{ current: id === currentLang }}
href={getPartialUrl(id)}
>
{formatLocale(id)}
</MasoActor>
))}
</div>
</Tooltip>
<p class="high-contrast-text">This content is available is {availableLanguages.length} languages.</p>
</div>
)
}
<Metadata
icon="material-symbols:person-outline"
title="Translators"
values={translators.map((id) => formatRecorder(id))}
/>
<Metadata
icon="material-symbols:person-edit-outline"
title="Transcribers"
values={transcribers.map((id) => formatRecorder(id))}
/>
<Metadata
icon="material-symbols:person-check-outline"
title="Proofreaders"
values={proofreaders.map((id) => formatRecorder(id))}
/>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#lang-selector {
display: flex;
align-items: center;
gap: 1em;
#tooltip-content {
display: grid;
gap: 0.5em;
& > .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
}
</style>

View File

@ -1,42 +1,38 @@
--- ---
import RichText from "components/RichText/RichText.astro"; import RichText from "components/RichText/RichText.astro";
import { payload } from "src/shared/payload/payload-sdk"; import { payload, type EndpointPage } from "src/shared/payload/payload-sdk";
import { getI18n } from "translations/translations"; import { getI18n } from "translations/translations";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro"; import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Metadata from "pages/[locale]/api/contents/_components/Metadata.astro";
import MasoTarget from "components/Maso/MasoTarget.astro"; import MasoTarget from "components/Maso/MasoTarget.astro";
import TagGroups from "components/TagGroups.astro";
import AppLayoutBackgroundImg from "components/AppLayout/components/AppLayoutBackgroundImg.astro"; import TableOfContent from "components/TableOfContent/TableOfContent.astro";
import LangCredits from "./_components/LangCredits.astro"; import LanguageOverride from "components/LanguageOverride.astro";
import Credits from "components/Credits.astro";
export const partial = true; export const partial = true;
interface Props { interface Props {
lang?: string; lang?: string;
slug?: string; slug?: string;
page?: EndpointPage;
} }
const reqUrl = new URL(Astro.request.url); const reqUrl = new URL(Astro.request.url);
const lang = Astro.props.lang ?? reqUrl.searchParams.get("lang")!; const lang = Astro.props.lang ?? reqUrl.searchParams.get("lang")!;
const slug = Astro.props.slug ?? reqUrl.searchParams.get("slug")!; const slug = Astro.props.slug ?? reqUrl.searchParams.get("slug")!;
const page = Astro.props.page ?? (await payload.getPage(slug));
const { getLocalizedUrl, formatCategory, formatContentType } = await getI18n( const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
Astro.locals.currentLocale
);
const { getLocalizedMatch } = await getI18n(lang); const { getLocalizedMatch } = await getI18n(lang);
const content = await payload.getContent(slug); const translation = getLocalizedMatch(page.translations, { title: slug });
const translation = getLocalizedMatch(content.translations, {
title: slug,
format: {},
sourceLanguage: "",
});
--- ---
<MasoTarget> {
{content.thumbnail && <AppLayoutBackgroundImg src={content.thumbnail.url} />} /* ------------------------------------------- HTML ------------------------------------------- */
}
<MasoTarget>
<div id="layout"> <div id="layout">
<div id="left"> <div id="left">
<AppLayoutTitle <AppLayoutTitle
@ -46,13 +42,13 @@ const translation = getLocalizedMatch(content.translations, {
/> />
{ {
content.thumbnail && ( page.thumbnail && (
<img <img
id="thumbnail" id="thumbnail"
class="when-not-large" class="when-not-large"
src={content.thumbnail.url} src={page.thumbnail.url}
width={content.thumbnail.width} width={page.thumbnail.width}
height={content.thumbnail.height} height={page.thumbnail.height}
/> />
) )
} }
@ -65,79 +61,71 @@ const translation = getLocalizedMatch(content.translations, {
) )
} }
{ <TagGroups tagGroups={page.tagGroups} />
(content.type || content.categories.length > 0) && (
<div class="meta-container">
{content.type && (
<Metadata
icon="material-symbols:shape-line-outline"
title="Type"
values={[formatContentType(content.type)]}
/>
)}
<Metadata
icon="material-symbols:workspaces-outline"
title="Categories"
values={content.categories.map((id) =>
formatCategory(id, "default")
)}
/>
</div>
)
}
<div class="when-not-large meta-container"> <div class="when-not-large meta-container">
<LangCredits {
page.translations.length > 1 && (
<LanguageOverride
currentLang={lang} currentLang={lang}
availableLanguages={content.translations.map( availableLanguages={page.translations.map(
({ language }) => language ({ language }) => language
)} )}
getPartialUrl={(lang) => getPartialUrl={(lang) =>
getLocalizedUrl(`/api/contents/partial?lang=${lang}&slug=${slug}`)} getLocalizedUrl(`/api/pages/partial?lang=${lang}&slug=${slug}`)
translators={translation.format.text?.translators} }
transcribers={translation.format.text?.transcribers} />
proofreaders={translation.format.text?.proofreaders} )
}
<Credits
translators={translation.translators}
proofreaders={translation.proofreaders}
/> />
</div> </div>
{ <div class="when-not-large meta-container">
translation.format.text && ( <TableOfContent toc={translation.toc} />
<> </div>
<hr /> <hr />
<div id="text"> <div id="text">
<RichText content={translation.format.text.content} /> <RichText content={translation.content} />
</div> </div>
</>
)
}
</div> </div>
<div id="right" class="when-large"> <div id="right" class="when-large">
{ {
content.thumbnail && ( page.thumbnail && (
<img <img
id="thumbnail" id="thumbnail"
src={content.thumbnail.url} src={page.thumbnail.url}
width={content.thumbnail.width} width={page.thumbnail.width}
height={content.thumbnail.height} height={page.thumbnail.height}
/> />
) )
} }
<div class="meta-container"> <div class="meta-container">
<LangCredits {
page.translations.length > 1 && (
<LanguageOverride
currentLang={lang} currentLang={lang}
availableLanguages={content.translations.map( availableLanguages={page.translations.map(
({ language }) => language ({ language }) => language
)} )}
getPartialUrl={(lang) => getPartialUrl={(lang) =>
getLocalizedUrl(`/api/contents/partial?lang=${lang}&slug=${slug}`)} getLocalizedUrl(`/api/pages/partial?lang=${lang}&slug=${slug}`)
translators={translation.format.text?.translators} }
transcribers={translation.format.text?.transcribers} />
proofreaders={translation.format.text?.proofreaders} )
}
<Credits
translators={translation.translators}
proofreaders={translation.proofreaders}
/> />
</div> </div>
<TableOfContent toc={translation.toc} />
</div> </div>
</div> </div>
</MasoTarget> </MasoTarget>

View File

@ -1,14 +0,0 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import Content from "src/pages/[locale]/api/contents/partial.astro";
const { slug } = Astro.params;
if (!slug) {
return Astro.redirect("/en/404");
}
---
<AppEmptyLayout>
<Content slug={slug} lang={Astro.locals.currentLocale} />
</AppLayout>

View File

@ -0,0 +1,18 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
<AppLayout pretitle="Guide to" title="Rich Text Editor" description="Having troubles using the Rich Text Editor? Looking for tips and advanced techniques? You've come to the right place!">
<div class="prose">
<h2>Add indentation / spaces between words</h2>
<p>
By default, additional spaces are collapsed. This means that if multiple
spaces are adjacent, only one space is preserved. To create spaces that
will not be collapsed, you can use tabs instead. Simply press the <kbd
>Tab</kbd
> key a few times on your keyboard to create additional spaces between words.
Be mindful of the use of these spaces.
</p>
</div>
</AppLayout>

View File

@ -4,21 +4,20 @@ import { payload } from "src/shared/payload/payload-sdk";
import { getI18n } from "translations/translations"; import { getI18n } from "translations/translations";
import RichText from "components/RichText/RichText.astro"; import RichText from "components/RichText/RichText.astro";
import FoldersSection from "./_components/FoldersSection.astro"; import FoldersSection from "./_components/FoldersSection.astro";
import { fetchOr404 } from "src/utils/responses";
import ErrorMessage from "components/ErrorMessage.astro";
const { slug } = Astro.params; const { slug } = Astro.params;
const { getLocalizedMatch, getLocalizedUrl } = await getI18n( const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
Astro.locals.currentLocale Astro.locals.currentLocale
); );
if (!slug) { const folder = await fetchOr404(() => payload.getFolder(slug!));
return Astro.redirect("/en/404"); if (folder instanceof Response) {
return folder;
} }
const folder = await payload.getFolder(slug);
const meta = getLocalizedMatch(folder.translations, { name: slug }); const meta = getLocalizedMatch(folder.translations, { name: slug });
// TODO: handle folder not found
// TODO: send description as RichTextContent instead of string
// TODO: handle light and dark illustration for applayout // TODO: handle light and dark illustration for applayout
--- ---
@ -30,7 +29,7 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
{ {
meta.description && ( meta.description && (
<div slot="header-description"> <div slot="header-description">
<RichText content={JSON.parse(meta.description)} /> <RichText content={meta.description} />
</div> </div>
) )
} }
@ -58,7 +57,8 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
<div> <div>
{ {
folder.files.map(({ relationTo, value }) => { folder.files.map(({ relationTo, value }) => {
if (relationTo === "contents") { switch (relationTo) {
case "contents":
return ( return (
<a <a
class="pressable" class="pressable"
@ -67,12 +67,28 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
{value.slug} {value.slug}
</a> </a>
); );
}
case "library-items":
return <p>Library item not supported yet! {value.slug}</p>;
case "pages":
return ( return (
<a href={getLocalizedUrl(`/library-item/${value.slug}`)}> <a
class="pressable"
href={getLocalizedUrl(`/pages/${value.slug}`)}
>
{value.slug} {value.slug}
</a> </a>
); );
default:
return (
<ErrorMessage
title={`Unknown file type: ${relationTo}`}
description="Please contact website technical administrator."
/>
);
}
}) })
} }
</div> </div>

View File

@ -9,6 +9,10 @@ interface Props {
const { icon = "material-symbols:folder-outline", title, href } = Astro.props; const { icon = "material-symbols:folder-outline", title, href } = Astro.props;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<a href={href} class="pressable"> <a href={href} class="pressable">
<Icon name={icon} /> <Icon name={icon} />
<div id="right"> <div id="right">
@ -16,6 +20,10 @@ const { icon = "material-symbols:folder-outline", title, href } = Astro.props;
</div> </div>
</a> </a>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
a { a {
display: flex; display: flex;

View File

@ -15,6 +15,10 @@ const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
); );
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div> <div>
{title && <h3>{title}</h3>} {title && <h3>{title}</h3>}
<section> <section>

View File

@ -2,11 +2,10 @@
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import AppLayout from "components/AppLayout/AppLayout.astro"; import AppLayout from "components/AppLayout/AppLayout.astro";
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import LinkCard from "../_components/LinkCard.astro";
import { getI18n } from "../../../translations/translations"; import { getI18n } from "../../../translations/translations";
import LibraryGrid from "./_components/LibraryGrid.astro";
import ChronicleCard from "pages/_components/ChronicleCard.astro"; import ChronicleCard from "./_components/ChronicleCard.astro";
import LibraryGrid from "pages/_components/LibraryGrid.astro"; import LinkCard from "./_components/LinkCard.astro";
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
--- ---
@ -46,7 +45,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<Button <Button
class="section-button" class="section-button"
title={t("home.librarySection.button")} title={t("home.librarySection.button")}
icon="material-symbols:browse-outline" icon="material-symbols:browse"
/> />
</a> </a>
<div class="grid"> <div class="grid">

View File

@ -0,0 +1,24 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import Page from "src/pages/[locale]/api/pages/partial.astro";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
const { slug } = Astro.params;
const page = await fetchOr404(() => payload.getPage(slug!));
if (page instanceof Response) {
return page;
}
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppEmptyLayout
parentPages={page.parentPages}
backgroundIllustration={page.thumbnail?.url}
>
<Page slug={page.slug} lang={Astro.locals.currentLocale} page={page} />
</AppEmptyLayout>

View File

@ -7,6 +7,10 @@ const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
const { t } = await getI18n(currentLocale); const { t } = await getI18n(currentLocale);
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title={t("settings.title")}> <AppLayout title={t("settings.title")}>
<div id="main"> <div id="main">
<div class="section"> <div class="section">
@ -69,6 +73,10 @@ const { t } = await getI18n(currentLocale);
</div> </div>
</AppLayout> </AppLayout>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
.section { .section {
display: flex; display: flex;

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@ import {
type EndpointKey, type EndpointKey,
type EndpointRecorder, type EndpointRecorder,
type Language, type Language,
type EndpointTag,
type EndpointTagsGroup,
type EndpointWording,
} from "src/shared/payload/payload-sdk"; } from "src/shared/payload/payload-sdk";
type Cache = { type Cache = {
@ -10,13 +13,19 @@ type Cache = {
currencies: string[]; currencies: string[];
keys: EndpointKey[]; keys: EndpointKey[];
recorders: EndpointRecorder[]; recorders: EndpointRecorder[];
tags: EndpointTag[];
tagsGroups: EndpointTagsGroup[];
wordings: EndpointWording[];
}; };
const fetchNewData = async (): Promise<Cache> => ({ const fetchNewData = async (): Promise<Cache> => ({
locales: (await payload.getLanguages()), locales: await payload.getLanguages(),
currencies: (await payload.getCurrencies()).map(({ id }) => id), currencies: (await payload.getCurrencies()).map(({ id }) => id),
keys: await payload.getKeys(), keys: await payload.getKeys(),
recorders: await payload.getRecorders(), recorders: await payload.getRecorders(),
tags: await payload.getTags(),
tagsGroups: await payload.getTagsGroups(),
wordings: await payload.getWordings(),
}); });
export let cache = await fetchNewData(); export let cache = await fetchNewData();

12
src/utils/responses.ts Normal file
View File

@ -0,0 +1,12 @@
export const fetchOr404 = async <T>(
promise: () => Promise<T>
): Promise<T | Response> => {
try {
return await promise();
} catch {
return new Response(null, {
status: 404,
statusText: "Not found",
});
}
};

View File

@ -1,30 +0,0 @@
import { expect, test } from "bun:test";
const cases: [string, string, string[], string][] = [
["", "", [], "/en/"],
["", "", ["fr"], "/fr/"],
["", "", ["en"], "/en/"],
["", "", ["en", "fr"], "/en/"],
["", "en", [], "/en/"],
["", "fr", [], "/fr/"],
["", "fr", ["en"], "/en/"],
["", "fr", ["en", "fr"], "/en/"],
["", "fr,en", ["en", "fr"], "/en/"],
];
test.each(cases)(
"Fetching url with prefix %p, with Accept-Language header %p, with cookie al_pref_languages %p, should redirect the user to %p",
async (urlPrefix, acceptLanguage, cookie, expectedRedirection) => {
const response = await fetch(`http://localhost:12498${urlPrefix}`, {
redirect: "manual",
headers: {
...(acceptLanguage ? { "Accept-Language": acceptLanguage } : {}),
...(cookie.length > 0
? { Cookie: `al_pref_languages=${JSON.stringify(cookie)}` }
: {}),
},
});
expect(response.status).toBe(302);
expect(response.headers.get("Location")).toBe(expectedRedirection);
}
);

View File

@ -5,7 +5,7 @@
"home.description": "We aim at archiving and translating all of <strong>Yoko Taro</strong>s works.<br />Yoko Taro is a Japanese video game director and scenario writer. He is best-known for his involvement with the <strong>NieR</strong > and <strong>Drakengard</strong> series. To complement his games, Yoko Taro likes to publish side materials in the form of books, anime, manga, audio books, novellas, even theater plays.<br />These media can be very difficult to find. His work goes all the way back to 2003. Most of it was released solely in Japanese, and sometimes in short supply. So this is what we do here: <strong>discover, archive, translate, and analyze</strong>.", "home.description": "We aim at archiving and translating all of <strong>Yoko Taro</strong>s works.<br />Yoko Taro is a Japanese video game director and scenario writer. He is best-known for his involvement with the <strong>NieR</strong > and <strong>Drakengard</strong> series. To complement his games, Yoko Taro likes to publish side materials in the form of books, anime, manga, audio books, novellas, even theater plays.<br />These media can be very difficult to find. His work goes all the way back to 2003. Most of it was released solely in Japanese, and sometimes in short supply. So this is what we do here: <strong>discover, archive, translate, and analyze</strong>.",
"home.aboutUsButton": "Read more about us", "home.aboutUsButton": "Read more about us",
"home.librarySection.title": "The Library", "home.librarySection.title": "The Library",
"home.librarySection.description": "Here you will find a list of IPs Yoko Taro worked on. Select one to discover all the media/content/articles that relates to this IP. <strong>Beware there can be spoilers.</strong>", "home.librarySection.description": "Here you will find a list of IPs Yoko Taro worked on. Select one to discover all the media/content/articles that relates to this IP. Alternatively you can also browse all content and use tags and filters to narrow your search. <strong>Beware there can be spoilers.</strong>",
"home.librarySection.button": "Browse all content", "home.librarySection.button": "Browse all content",
"home.chroniclesSection.title": "The Chronicles", "home.chroniclesSection.title": "The Chronicles",
"home.chroniclesSection.description": "Interested in exploring the Yokoverse lore? Experience all events and content in chronological order. <strong>Beware there can be spoilers.</strong>", "home.chroniclesSection.description": "Interested in exploring the Yokoverse lore? Experience all events and content in chronological order. <strong>Beware there can be spoilers.</strong>",
@ -55,5 +55,7 @@
"footer.license.description": "This websites content is made available under <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\">CC-BY-SA</a> unless otherwise noted.", "footer.license.description": "This websites content is made available under <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\">CC-BY-SA</a> unless otherwise noted.",
"footer.license.icons.tooltip": "CC-BY-SA 4.0 License", "footer.license.icons.tooltip": "CC-BY-SA 4.0 License",
"footer.disclaimer": "<strong>Accords Library</strong> is not affiliated with or endorsed by <strong>SQUARE ENIX CO. LTD</strong>. All game assets and promotional materials belongs to <strong>© SQUARE ENIX CO. LTD</strong>." "footer.disclaimer": "<strong>Accords Library</strong> is not affiliated with or endorsed by <strong>SQUARE ENIX CO. LTD</strong>. All game assets and promotional materials belongs to <strong>© SQUARE ENIX CO. LTD</strong>.",
"header.nav.parentPages.label": "{{ count }} parent page{{ count+,>1{s} }}"
} }

View File

@ -115,7 +115,8 @@ export const getI18n = async (locale: string) => {
fallback: Omit<T, "language"> fallback: Omit<T, "language">
): Omit<T, "language"> & { language?: string } => ): Omit<T, "language"> & { language?: string } =>
options.find(({ language }) => language === locale) ?? options.find(({ language }) => language === locale) ??
options.find(({ language }) => language === defaultLocale) ?? { options.find(({ language }) => language === defaultLocale) ??
options[0] ?? {
...fallback, ...fallback,
}; };
@ -152,6 +153,16 @@ export const getI18n = async (locale: string) => {
}, },
getLocalizedUrl: (url: string): string => `/${locale}${url}`, getLocalizedUrl: (url: string): string => `/${locale}${url}`,
getLocalizedMatch, getLocalizedMatch,
formatTag: (id: string): string => {
const tag = cache.tags.find(({ slug }) => slug === id);
if (!tag) return "UNKNOWN";
return getLocalizedMatch(tag.translations, { name: tag.slug }).name;
},
formatTagsGroup: (id: string): string => {
const tag = cache.tagsGroups.find(({ slug }) => slug === id);
if (!tag) return "UNKNOWN";
return getLocalizedMatch(tag.translations, { name: tag.slug }).name;
},
formatCategory: ( formatCategory: (
id: string, id: string,
format: "short" | "default" = "default" format: "short" | "default" = "default"
@ -206,11 +217,12 @@ export const getCurrentLocale = (pathname: string): Locale | undefined => {
export const getBestAcceptedLanguage = ( export const getBestAcceptedLanguage = (
request: Request request: Request
): Locale | undefined => { ): Locale | undefined => {
const header = request.headers.get("Accept-Language");
if (!header) return;
acceptLanguage.languages(cache.locales.map(({ id }) => id)); acceptLanguage.languages(cache.locales.map(({ id }) => id));
return ( return (
(acceptLanguage.get( acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined
request.headers.get("Accept-Language")
) as Locale | null) ?? undefined
); );
}; };