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: {
port: 12499,
host: true,

BIN
bun.lockb

Binary file not shown.

View File

@ -10,15 +10,20 @@
"astro": "astro",
"upgrade": "ncu",
"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": {
"@astrojs/check": "^0.5.4",
"@astrojs/node": "^8.2.0",
"@astrojs/check": "^0.5.6",
"@astrojs/node": "^8.2.1",
"@fontsource-variable/murecho": "^5.0.17",
"@fontsource-variable/vollkorn": "^5.0.19",
"accept-language": "^3.0.18",
"astro": "4.3.7",
"astro": "4.4.6",
"astro-icon": "^1.1.0",
"node-cache": "^5.1.2",
"tippy.js": "^6.3.7",
@ -26,13 +31,13 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.1.72",
"@iconify-json/material-symbols": "^1.1.73",
"@types/ua-parser-js": "^0.7.39",
"astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17",
"bun-types": "^1.0.26",
"bun-types": "^1.0.29",
"npm-check-updates": "^16.14.15",
"postcss-preset-env": "^9.3.0",
"postcss-preset-env": "^9.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}

View File

@ -1,16 +1,23 @@
---
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 type { ComponentProps } from "astro/types";
import type { ParentPage } from "src/shared/payload/payload-sdk";
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
interface Props {
parentPages?: ComponentProps<typeof Topbar>["parentPages"];
parentPages?: ParentPage[];
metaTitle?: string;
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}>
<header>
{
backgroundIllustration && (
<AppLayoutBackgroundImg src={backgroundIllustration} />
)
}
<Topbar parentPages={parentPages} />
</header>
<main><slot /></main>

View File

@ -1,7 +1,7 @@
---
import Html from "./components/Html.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 AppLayoutTitle from "./components/AppLayoutTitle.astro";
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" />
<noscript set:html={styleNoScript} />
{
@ -36,7 +35,6 @@ const styleNoScript = `
img {
opacity: 0;
transition: 3s opacity;
position: absolute;
top: 0;
left: 0;
@ -54,6 +52,10 @@ const styleNoScript = `
}
</style>
{
/* ------------------------------------------- JS --------------------------------------------- */
}
<script define:vars={{ uniqueId }}>
const element = document.getElementById(uniqueId);

View File

@ -8,12 +8,20 @@ interface Props {
const { title, subtitle, pretitle } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<h1 class="high-contrast-text">
{pretitle && <span id="pretitle">{pretitle}&nbsp;</span>}
<span id="title">{title}&nbsp;</span>
{subtitle && <span id="subtitle">{subtitle}</span>}
</h1>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
h1 {
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")} />
</footer>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
footer {
border-top: 0.1em solid var(--color-base-1000);
@ -251,6 +255,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
& > #license-section {
grid-area: license;
line-height: 1.2;
& > #common-creative {
display: flex;
@ -288,6 +293,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
@media (max-width: 35rem) {
place-content: center;
flex-wrap: wrap;
gap: clamp(24px, 8vw, 48px);
& > a > svg {
@ -301,6 +307,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
& > #copyright {
border-left: 0.1em solid var(--color-base-1000);
padding-left: 1em;
line-height: 1.2;
@media (max-width: 35rem) {
border: none;

View File

@ -16,7 +16,8 @@ const isIOS = parser.getOS().name === "iOS";
const { currentTheme } = Astro.locals;
/* -------------------------------------------- HTML -------------------------------------------- */
/* Keep that separator here or else it breaks the HTML
----------------------------------------------- HTML -------------------------------------------- */
---
<html
@ -46,8 +47,14 @@ const { currentTheme } = Astro.locals;
/>
<link rel="manifest" href="/site.webmanifest" />
<style is:global>
.when-no-js {
display: none;
}
</style>
<noscript>
<style>
<style is:global>
.when-js {
display: none !important;
}
@ -69,12 +76,6 @@ const { currentTheme } = Astro.locals;
}
<style is:global>
@media print {
.when-no-print {
display: none !important;
}
}
html {
--color-base-0: #ffffff;
--color-base-50: #fffaf3;
@ -107,6 +108,8 @@ const { currentTheme } = Astro.locals;
--color-shadow-1: var(--color-base-350);
--color-shadow-2: var(--color-base-300);
--color-critical-error: #940000;
--texture-dots: url(/img/paper-dots.webp);
--texture-dots-blend: multiply;
@ -157,6 +160,8 @@ const { currentTheme } = Astro.locals;
--color-shadow-1: var(--color-base-0);
--color-shadow-2: var(--color-base-50);
--color-critical-error: red;
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
@ -170,57 +175,6 @@ const { currentTheme } = Astro.locals;
}
&: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) {
--color-base-1000: #ebeae7;
--color-base-950: #eae5e0;
@ -253,6 +207,8 @@ const { currentTheme } = Astro.locals;
--color-shadow-1: var(--color-base-0);
--color-shadow-2: var(--color-base-50);
--color-critical-error: red;
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
@ -341,8 +297,21 @@ const { currentTheme } = Astro.locals;
p {
& a {
transition-duration: 150ms;
transition-property: text-decoration-color, color;
color: var(--color-base-750);
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 {
--foreground-color: var(--color-base-650);
color: var(--foreground-color);
@ -396,17 +388,13 @@ const { currentTheme } = Astro.locals;
}
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.when-no-js {
display: none;
}
.prose {
font-size: 16px;
line-height: 1.75;
@ -425,6 +413,12 @@ const { currentTheme } = Astro.locals;
> p {
margin-top: 1.25em;
margin-bottom: 1.25em;
> kbd {
background-color: var(--color-shadow-2);
padding: 0.15em 0.3em;
border-radius: 0.3em;
}
}
> h2 {
@ -443,6 +437,7 @@ const { currentTheme } = Astro.locals;
margin-top: 2em;
margin-bottom: 1em;
line-height: 1;
scroll-margin: 1em;
}
> h2 + h3,
@ -452,4 +447,16 @@ const { currentTheme } = Astro.locals;
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>

View File

@ -1,14 +1,15 @@
---
import { Icon } from "astro-icon/components";
import Button from "components/Button.astro";
import ThemeSelector from "components/AppLayout/components/ThemeSelector.astro";
import LanguageSelector from "components/AppLayout/components/LanguageSelector.astro";
import CurrencySelector from "components/AppLayout/components/CurrencySelector.astro";
import ThemeSelector from "./components/ThemeSelector.astro";
import LanguageSelector from "./components/LanguageSelector.astro";
import CurrencySelector from "./components/CurrencySelector.astro";
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 {
parentPages?: { name: string; slug: string; type: string }[] | undefined;
parentPages?: ParentPage[] | undefined;
hideHomeButton?: boolean;
}
@ -24,31 +25,20 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<nav id="topbar" class="when-no-print">
{
(!hideHomeButton || parentPages.length > 0) && (
<div id="breadcrumb" class="hide-scrollbar high-contrast-text">
<a href="/">
<div id="left" class="hide-scrollbar high-contrast-text">
<a href="/" class="pressable-label">
<Icon name="material-symbols:home" width={16} height={16} />
<p>{t("home.title")}</p>
</a>
{parentPages.length > 0 && (
<Tooltip trigger="click">
<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>
<ParentPagesButton parentPages={parentPages} />
)}
</div>
)
}
<div id="toolbar">
<div id="toolbar" class="hide-scrollbar">
<a href={getLocalizedUrl("/search")}>
<Button
icon="material-symbols:search"
@ -89,40 +79,16 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
flex-wrap: wrap-reverse;
gap: 32px 64px;
& > #breadcrumb {
& > #left {
display: flex;
place-items: center;
overflow-x: scroll;
gap: 8px;
margin-left: -0.8em;
& > svg {
& > :global(*) {
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 {
@ -131,6 +97,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
gap: 12px;
place-items: center;
justify-content: flex-end;
overflow-x: scroll;
@media (max-width: 28rem) {
justify-content: center;
@ -138,7 +105,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
@media (max-width: 22rem) {
justify-content: space-between;
gap: 0;
gap: 12px;
}
& > .separator {
@ -172,11 +139,3 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
}
}
</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;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
{
condition ? (
<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>
class MasoTarget extends HTMLElement {}
customElements.define("maso-target", MasoTarget);
import { customElement } from "src/utils/customElements";
customElement("maso-target");
</script>

View File

@ -12,6 +12,10 @@ const { icon, title, values } = Astro.props;
if (values.length === 0) return;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div id="container">
<div id="title">
<Icon name={icon} width={24} height={24} />
@ -22,13 +26,17 @@ if (values.length === 0) return;
</div>
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: center;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}

View File

@ -2,41 +2,34 @@
import type { RichTextContext } from "src/utils/richText";
import RTSection from "./components/RTSection.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 {
node: {
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
};
};
node: RichTextBlockNode;
context: RichTextContext;
}
const { node, context } = Astro.props;
let NodeElement;
switch (node.fields.blockType) {
case "sectionBlock":
NodeElement = RTSection;
break;
case "transcriptBlock":
NodeElement = RTTranscript;
break;
}
---
{
NodeElement ? (
<NodeElement node={node} context={context} />
isBlockNodeSectionBlock(node) ? (
<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 type { RichTextContent } from "src/shared/payload/payload-sdk";
import type {
RichTextSectionBlock,
} from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText";
interface Props {
node: {
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
lines: RichTextContent;
};
};
node: RichTextSectionBlock;
context: RichTextContext;
}
@ -25,19 +15,43 @@ const { node, context } = Astro.props;
{
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 ? (
<h3>{node.fields.blockName}</h3>
<h3 id={node.fields.anchorHash}>
<span>{`${node.fields.anchorHash} `}</span>
{node.fields.blockName}
</h3>
) : 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 ? (
<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
content={node.fields.lines}
content={node.fields.content}
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 RTLine from "./RTLine.astro";
import RTCue from "./RTCue.astro";
import type { RichTextTranscriptBlock } from "src/shared/payload/payload-sdk";
import Block from "components/Blocks/Block.astro";
interface Props {
node: {
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
lines: { blockType: string }[];
};
};
node: RichTextTranscriptBlock;
context: RichTextContext;
}
const { node, context } = Astro.props;
const { node } = Astro.props;
---
<div>
{
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>
);
}
})
}
{node.fields.lines.map((block) => <Block block={block} />)}
</div>
<style>
div {
padding-block: 1em;
display: grid;
grid-template-columns: auto 1fr;
grid-template-columns: [name] auto [text] 1fr;
gap: 1.5em 2em;
}
</style>

View File

@ -3,24 +3,15 @@ import type { RichTextContext } from "src/utils/richText";
import RTNode from "../RTNode.astro";
import RTCustomLink from "./components/RTCustomLink.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 {
node: {
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;
};
node: RichTextLinkNode;
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}>
{node.children.map((node) => (
<RTNode node={node} context={context} />
))}
</RTCustomLink>
) : node.fields.linkType === "internal" ? (
) : isLinkNodeInternalLinkNode(node) ? (
<RTInternalLink doc={node.fields.doc}>
{node.children.map((node) => (
<RTNode node={node} context={context} />
))}
</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";
interface Props {
@ -18,6 +19,9 @@ const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<slot />
</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 RTBasicListItem from "./components/RTBasicListItem.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 {
node: {
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;
};
node: RichTextListNode;
context: RichTextContext;
}
@ -27,27 +19,28 @@ const { node, context } = Astro.props;
---
{
node.listType === "number" ? (
isListNodeNumberListNode(node) ? (
<ol>
{node.children.map((node) => (
<RTBasicListItem node={node} context={context} />
))}
</ol>
) : node.listType === "bullet" ? (
) : isListNodeBulletListNode(node) ? (
<ul>
{node.children.map((node) => (
<RTBasicListItem node={node} context={context} />
))}
</ul>
) : node.listType === "check" ? (
) : isListNodeCheckListNode(node) ? (
<ul>
{node.children.map((node) => (
<RTCheckListItem node={node} context={context} />
))}
</ul>
) : (
<p>
{`Unknown list type: ${node.listType}. Please contact website technical administrator.`}
</p>
<ErrorMessage
title={`Unknown list link: ${node.listType}`}
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 RTBlock from "./RTBlock/RTBlock.astro";
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 {
node: {
type: string;
version: number;
[k: string]: unknown;
};
node: RichTextNode;
context: RichTextContext;
}
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 ? (
<NodeElement node={node} context={context} />
/* ------------------------------------------- HTML ------------------------------------------- */
}
{
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 RTNode from "./RTNode.astro";
import type { RichTextParagraphNode } from "src/shared/payload/payload-sdk";
interface Props {
node: {
type: string;
version: number;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
[k: string]: unknown;
};
node: RichTextParagraphNode;
context: RichTextContext;
}
@ -23,9 +15,12 @@ const { node, context } = Astro.props;
/* ------------------------------------------- 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 RTSuperscript from "./components/RTSuperscript.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 {
node: {
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
};
node: RichTextTextNode;
context: RichTextContext;
}
const { node } = Astro.props;
@ -23,8 +20,14 @@ const { node } = Astro.props;
<ConditionalWrapper wrapper={RTBold} condition={Boolean(node.format & 1)}>
<ConditionalWrapper wrapper={RTItalic} condition={Boolean(node.format & 2)}>
<ConditionalWrapper wrapper={RTLineThrough} condition={Boolean(node.format & 4)}>
<ConditionalWrapper wrapper={RTUnderline} condition={Boolean(node.format & 8)}>
<ConditionalWrapper
wrapper={RTLineThrough}
condition={Boolean(node.format & 4)}
>
<ConditionalWrapper
wrapper={RTUnderline}
condition={Boolean(node.format & 8)}
>
<ConditionalWrapper
wrapper={RTInlineCode}
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 acceptedLocale = getBestAcceptedLanguage(request);
const cookieLocale = getCookieLocale(cookies);
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? defaultLocale;
const bestMatchingLocale =
cookieLocale ?? acceptedLocale ?? currentLocale ?? defaultLocale;
if (!currentLocale) {
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);

View File

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

View File

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

View File

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

View File

@ -10,6 +10,10 @@ interface Props {
const { icon, subtitle, title, href } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<a href={href} class="pressable">
<Icon name={icon} />
<div id="right">
@ -18,6 +22,10 @@ const { icon, subtitle, title, href } = Astro.props;
</div>
</a>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
a {
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 { payload } from "src/shared/payload/payload-sdk";
import { payload, type EndpointPage } from "src/shared/payload/payload-sdk";
import { getI18n } from "translations/translations";
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 AppLayoutBackgroundImg from "components/AppLayout/components/AppLayoutBackgroundImg.astro";
import LangCredits from "./_components/LangCredits.astro";
import TagGroups from "components/TagGroups.astro";
import TableOfContent from "components/TableOfContent/TableOfContent.astro";
import LanguageOverride from "components/LanguageOverride.astro";
import Credits from "components/Credits.astro";
export const partial = true;
interface Props {
lang?: string;
slug?: string;
page?: EndpointPage;
}
const reqUrl = new URL(Astro.request.url);
const lang = Astro.props.lang ?? reqUrl.searchParams.get("lang")!;
const slug = Astro.props.slug ?? reqUrl.searchParams.get("slug")!;
const page = Astro.props.page ?? (await payload.getPage(slug));
const { getLocalizedUrl, formatCategory, formatContentType } = await getI18n(
Astro.locals.currentLocale
);
const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch } = await getI18n(lang);
const content = await payload.getContent(slug);
const translation = getLocalizedMatch(content.translations, {
title: slug,
format: {},
sourceLanguage: "",
});
const translation = getLocalizedMatch(page.translations, { title: slug });
---
<MasoTarget>
{content.thumbnail && <AppLayoutBackgroundImg src={content.thumbnail.url} />}
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<MasoTarget>
<div id="layout">
<div id="left">
<AppLayoutTitle
@ -46,13 +42,13 @@ const translation = getLocalizedMatch(content.translations, {
/>
{
content.thumbnail && (
page.thumbnail && (
<img
id="thumbnail"
class="when-not-large"
src={content.thumbnail.url}
width={content.thumbnail.width}
height={content.thumbnail.height}
src={page.thumbnail.url}
width={page.thumbnail.width}
height={page.thumbnail.height}
/>
)
}
@ -65,79 +61,71 @@ const translation = getLocalizedMatch(content.translations, {
)
}
{
(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>
)
}
<TagGroups tagGroups={page.tagGroups} />
<div class="when-not-large meta-container">
<LangCredits
currentLang={lang}
availableLanguages={content.translations.map(
({ language }) => language
)}
getPartialUrl={(lang) =>
getLocalizedUrl(`/api/contents/partial?lang=${lang}&slug=${slug}`)}
translators={translation.format.text?.translators}
transcribers={translation.format.text?.transcribers}
proofreaders={translation.format.text?.proofreaders}
{
page.translations.length > 1 && (
<LanguageOverride
currentLang={lang}
availableLanguages={page.translations.map(
({ language }) => language
)}
getPartialUrl={(lang) =>
getLocalizedUrl(`/api/pages/partial?lang=${lang}&slug=${slug}`)
}
/>
)
}
<Credits
translators={translation.translators}
proofreaders={translation.proofreaders}
/>
</div>
{
translation.format.text && (
<>
<hr />
<div id="text">
<RichText content={translation.format.text.content} />
</div>
</>
)
}
<div class="when-not-large meta-container">
<TableOfContent toc={translation.toc} />
</div>
<hr />
<div id="text">
<RichText content={translation.content} />
</div>
</div>
<div id="right" class="when-large">
{
content.thumbnail && (
page.thumbnail && (
<img
id="thumbnail"
src={content.thumbnail.url}
width={content.thumbnail.width}
height={content.thumbnail.height}
src={page.thumbnail.url}
width={page.thumbnail.width}
height={page.thumbnail.height}
/>
)
}
<div class="meta-container">
<LangCredits
currentLang={lang}
availableLanguages={content.translations.map(
({ language }) => language
)}
getPartialUrl={(lang) =>
getLocalizedUrl(`/api/contents/partial?lang=${lang}&slug=${slug}`)}
translators={translation.format.text?.translators}
transcribers={translation.format.text?.transcribers}
proofreaders={translation.format.text?.proofreaders}
{
page.translations.length > 1 && (
<LanguageOverride
currentLang={lang}
availableLanguages={page.translations.map(
({ language }) => language
)}
getPartialUrl={(lang) =>
getLocalizedUrl(`/api/pages/partial?lang=${lang}&slug=${slug}`)
}
/>
)
}
<Credits
translators={translation.translators}
proofreaders={translation.proofreaders}
/>
</div>
<TableOfContent toc={translation.toc} />
</div>
</div>
</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 RichText from "components/RichText/RichText.astro";
import FoldersSection from "./_components/FoldersSection.astro";
import { fetchOr404 } from "src/utils/responses";
import ErrorMessage from "components/ErrorMessage.astro";
const { slug } = Astro.params;
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
Astro.locals.currentLocale
);
if (!slug) {
return Astro.redirect("/en/404");
const folder = await fetchOr404(() => payload.getFolder(slug!));
if (folder instanceof Response) {
return folder;
}
const folder = await payload.getFolder(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
---
@ -30,7 +29,7 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
{
meta.description && (
<div slot="header-description">
<RichText content={JSON.parse(meta.description)} />
<RichText content={meta.description} />
</div>
)
}
@ -58,21 +57,38 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
<div>
{
folder.files.map(({ relationTo, value }) => {
if (relationTo === "contents") {
return (
<a
class="pressable"
href={getLocalizedUrl(`/contents/${value.slug}`)}
>
{value.slug}
</a>
);
switch (relationTo) {
case "contents":
return (
<a
class="pressable"
href={getLocalizedUrl(`/contents/${value.slug}`)}
>
{value.slug}
</a>
);
case "library-items":
return <p>Library item not supported yet! {value.slug}</p>;
case "pages":
return (
<a
class="pressable"
href={getLocalizedUrl(`/pages/${value.slug}`)}
>
{value.slug}
</a>
);
default:
return (
<ErrorMessage
title={`Unknown file type: ${relationTo}`}
description="Please contact website technical administrator."
/>
);
}
return (
<a href={getLocalizedUrl(`/library-item/${value.slug}`)}>
{value.slug}
</a>
);
})
}
</div>

View File

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

View File

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

View File

@ -2,11 +2,10 @@
import { Icon } from "astro-icon/components";
import AppLayout from "components/AppLayout/AppLayout.astro";
import Button from "components/Button.astro";
import LinkCard from "../_components/LinkCard.astro";
import { getI18n } from "../../../translations/translations";
import ChronicleCard from "pages/_components/ChronicleCard.astro";
import LibraryGrid from "pages/_components/LibraryGrid.astro";
import LibraryGrid from "./_components/LibraryGrid.astro";
import ChronicleCard from "./_components/ChronicleCard.astro";
import LinkCard from "./_components/LinkCard.astro";
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
---
@ -46,7 +45,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<Button
class="section-button"
title={t("home.librarySection.button")}
icon="material-symbols:browse-outline"
icon="material-symbols:browse"
/>
</a>
<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);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title={t("settings.title")}>
<div id="main">
<div class="section">
@ -69,6 +73,10 @@ const { t } = await getI18n(currentLocale);
</div>
</AppLayout>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
.section {
display: flex;

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@ import {
type EndpointKey,
type EndpointRecorder,
type Language,
type EndpointTag,
type EndpointTagsGroup,
type EndpointWording,
} from "src/shared/payload/payload-sdk";
type Cache = {
@ -10,13 +13,19 @@ type Cache = {
currencies: string[];
keys: EndpointKey[];
recorders: EndpointRecorder[];
tags: EndpointTag[];
tagsGroups: EndpointTagsGroup[];
wordings: EndpointWording[];
};
const fetchNewData = async (): Promise<Cache> => ({
locales: (await payload.getLanguages()),
locales: await payload.getLanguages(),
currencies: (await payload.getCurrencies()).map(({ id }) => id),
keys: await payload.getKeys(),
recorders: await payload.getRecorders(),
tags: await payload.getTags(),
tagsGroups: await payload.getTagsGroups(),
wordings: await payload.getWordings(),
});
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.aboutUsButton": "Read more about us",
"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.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>",
@ -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.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">
): Omit<T, "language"> & { language?: string } =>
options.find(({ language }) => language === locale) ??
options.find(({ language }) => language === defaultLocale) ?? {
options.find(({ language }) => language === defaultLocale) ??
options[0] ?? {
...fallback,
};
@ -152,6 +153,16 @@ export const getI18n = async (locale: string) => {
},
getLocalizedUrl: (url: string): string => `/${locale}${url}`,
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: (
id: string,
format: "short" | "default" = "default"
@ -206,11 +217,12 @@ export const getCurrentLocale = (pathname: string): Locale | undefined => {
export const getBestAcceptedLanguage = (
request: Request
): Locale | undefined => {
const header = request.headers.get("Accept-Language");
if (!header) return;
acceptLanguage.languages(cache.locales.map(({ id }) => id));
return (
(acceptLanguage.get(
request.headers.get("Accept-Language")
) as Locale | null) ?? undefined
acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined
);
};