Added content

This commit is contained in:
DrMint 2024-02-26 03:08:59 +01:00
parent 8fc4b6a5c4
commit 133fa015e5
51 changed files with 1498 additions and 420 deletions

View File

@ -8,27 +8,29 @@
- `when-dark-theme`: only display element if the current theme is dark (manually or automatically)
- `when-light-theme`: only display element if the current theme is light (manually or automatically)
- `when-no-print`: only display when not printing
- `hide-scrollbar`: hide the element scrollbar
- `texture-dots`: add a background paper like texture to the element
- `font-serif`: by default, everything use sans-serif. Use this class to make the font serif.
- `high-contrast-text`: add a shadow around the text to increase perceived contrast.
- `prose`: apply typography rules. Useful for main text content
## CSS Component classes
- `pressable-icon`: used to make a SVG/Text look pressable
- `keycap`: used to make an element look like a pressable keycap
- `pressable`: used to make a container look pressable
## CSS Global Variables
- `--color-base-X`: the current theme colors. X can be between 0 and 1000, available in increments of 50.
- `--font-serif`: by default, everything use sans-serif. Use this variable to make the font serif.
## Translations
For all the following exemples, the spaces within the double curly braces are important.
### Variables
Variables allow to embed strings or numbers within a translation.
@ -116,4 +118,4 @@ If then you call:
It will produce
`Return to Home`
`Return back`
`Return back`

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/img/bg-home2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

59
public/site.webmanifest Normal file
View File

@ -0,0 +1,59 @@
{
"background_color": "#27231e",
"theme_color": "#27231e",
"categories": ["books", "education", "entertainment", "news", "games"],
"description": "Accord's Library aims at gathering and archiving all of Yoko Taros work. Yoko Taro is a Japanese video game director and scenario writer.",
"dir": "auto",
"display": "standalone",
"icons": [
{
"src": "/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"name": "Accord's Library",
"short_name": "Accord's Lib",
"start_url": ".",
"shortcuts": [
{
"name": "Library",
"url": "/library",
"description": "Browse all physical and digital media"
},
{
"name": "Contents",
"url": "/contents",
"description": "Explore all content and filter by type or category"
},
{
"name": "Wiki",
"url": "/wiki",
"description": "An encyclopedia for everything related to DrakeNieR"
},
{
"name": "Chronicles",
"url": "/chronicles",
"description": "Experience all events and content in chronological order"
},
{
"name": "News",
"url": "/news",
"description": "All the latest info"
},
{
"name": "Gallery",
"url": "/gallery",
"description": "Thousands of offcial artworks"
}
]
}

View File

@ -0,0 +1,44 @@
---
import Html from "./components/Html.astro";
import Topbar from "./components/Topbar.astro";
import Footer from "./components/Footer.astro";
import type { ComponentProps } from "astro/types";
interface Props {
parentPages?: ComponentProps<typeof Topbar>["parentPages"];
metaTitle?: string;
hideFooterLinks?: boolean;
}
const { metaTitle, hideFooterLinks = false, parentPages } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Html title={metaTitle}>
<header>
<Topbar parentPages={parentPages} />
</header>
<main><slot /></main>
<Footer withLinks={!hideFooterLinks} />
</Html>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
header {
display: flex;
flex-direction: column;
gap: 1.5em;
}
main {
padding-top: 1em;
padding-bottom: 8em;
flex-grow: 1;
}
</style>

View File

@ -1,24 +1,37 @@
---
import Html from "./components/Html.astro";
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
import Topbar from "./components/Topbar.astro";
import Footer from "./components/Footer.astro";
import AppLayoutTitle from "./components/AppLayoutTitle.astro";
import type { ComponentProps } from "astro/types";
interface Props {
breadcrumb?: { name: string; slug: string }[];
title?: string | undefined;
parentPages?: ComponentProps<typeof Topbar>["parentPages"];
pretitle?: string | undefined;
title: string;
subtitle?: string | undefined;
description?: string | undefined;
illustration?: string;
illustrationSize?: string;
illustrationPosition?: string;
backgroundIllustration?: string | undefined;
hideFooterLinks?: boolean;
hideHomeButton?: boolean;
}
const {
title = "Accords Library",
title,
subtitle,
pretitle,
description,
illustration,
breadcrumb = [],
backgroundIllustration,
parentPages,
illustrationSize = "contain",
illustrationPosition = "center",
hideFooterLinks = false,
hideHomeButton = false,
} = Astro.props;
---
@ -28,24 +41,38 @@ const {
<Html title={title}>
<header>
<Topbar breadcrumb={breadcrumb} />
<div id="header-content">
<div id="header-left">
<slot name="header-title">
<h1>{title}</h1>
</slot>
{
backgroundIllustration && (
<AppLayoutBackgroundImg src={backgroundIllustration} />
)
}
<div id="description">
<slot name="header-description">
<p>{description}</p>
</slot>
<Topbar parentPages={parentPages} hideHomeButton={hideHomeButton} />
{
(
<div id="header-content">
<div id="header-left">
<slot name="header-title">
<AppLayoutTitle
pretitle={pretitle}
title={title}
subtitle={subtitle}
/>
</slot>
<div class="prose">
<slot name="header-description">
<p>{description}</p>
</slot>
</div>
</div>
{illustration && <div id="image-container" />}
</div>
</div>
{illustration && <div id="image-container" />}
</div>
)
}
</header>
<main><slot /></main>
<Footer withLinks={breadcrumb.length > 0} />
<Footer withLinks={!hideFooterLinks} />
</Html>
{
@ -74,20 +101,6 @@ const {
flex-direction: column;
gap: 2em;
place-items: flex-start;
& > h1 {
font-family: var(--font-serif);
font-size: 3em;
overflow-wrap: anywhere;
}
& > #description {
max-width: 35em;
& > p {
line-height: 1.4;
}
}
}
& > #image-container {

View File

@ -0,0 +1,63 @@
---
import { getRandomId } from "src/utils/random";
interface Props {
src: string;
alt?: string;
id?: string;
class?: string;
}
const { src, alt } = Astro.props;
const uniqueId = getRandomId();
const styleNoScript = `
<style>
#${uniqueId} {
opacity: 1;
transition: unset;
}
</style>`;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<img id={uniqueId} src={src} alt={alt} class="when-no-print" />
<noscript set:html={styleNoScript} />
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
img {
opacity: 0;
transition: 3s opacity;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
height: 100vh;
object-fit: cover;
object-position: 50% 0;
width: 100%;
mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0) 100%
);
}
</style>
<script define:vars={{ uniqueId }}>
const element = document.getElementById(uniqueId);
element.addEventListener("load", () => {
element.style.opacity = 1;
});
</script>

View File

@ -0,0 +1,41 @@
---
interface Props {
pretitle?: string | undefined;
title: string;
subtitle?: string | undefined;
}
const { title, subtitle, pretitle } = Astro.props;
---
<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>
<style>
h1 {
line-height: 0.8;
display: grid;
overflow-wrap: anywhere;
font-size: clamp(1em, 0.7em + 1.5vw, 2em);
& > #pretitle {
font-family: var(--font-sans-serifs);
font-weight: 400;
margin-bottom: 0.8em;
}
& > #title {
font-family: var(--font-serif);
font-size: 200%;
}
& > #subtitle {
font-family: var(--font-serif);
font-weight: 600;
margin-top: 0.5em;
}
}
</style>

View File

@ -3,6 +3,7 @@ import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { cache } from "src/utils/cachedPayload";
import { getI18n } from "translations/translations";
import { formatCurrency } from "src/utils/currencies";
interface Props {
withTitle?: boolean | undefined;
@ -28,7 +29,7 @@ const { currentCurrency } = Astro.locals;
href={`?action-currency=${id}`}
data-astro-prefetch="tap"
>
{id}
{`${id} (${formatCurrency(id)})`}
</a>
))
}

View File

@ -33,7 +33,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
<footer>
{
withLinks && (
<div id="nav">
<div id="nav" class="when-no-print">
<p class="font-serif">{t("global.siteName")}</p>
<div>
<a href="/">
@ -41,11 +41,11 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
<p>{t("footer.links.home.title")}</p>
</a>
<a href="/archives">
<Icon name="material-symbols:browse-outline" />
<Icon name="material-symbols:browse" />
<p>{"Contents"}</p>
</a>
<a href="/chronicles">
<Icon name="material-symbols:book-2-outline" />
<Icon name="material-symbols:book-2" />
<p>{"Chronicles"}</p>
</a>
<a href="/changelog">
@ -53,19 +53,19 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
<p>{"Changelog"}</p>
</a>
<a href="/timeline">
<Icon name="material-symbols:calendar-month-outline" />
<Icon name="material-symbols:calendar-month" />
<p>{t("footer.links.timeline.title")}</p>
</a>
<a href="https://gallery.accords-library.com/posts">
<Icon name="material-symbols:perm-media-outline" />
<Icon name="material-symbols:perm-media" />
<p>{t("footer.links.gallery.title")}</p>
</a>
<a href="/videos">
<Icon name="material-symbols:movie-outline" />
<Icon name="material-symbols:movie" />
<p>{t("footer.links.videos.title")}</p>
</a>
<a href="/archives">
<Icon name="material-symbols:folder-zip-outline" />
<Icon name="material-symbols:folder-zip" />
<p>{t("footer.links.webArchives.title")}</p>
</a>
</div>
@ -90,7 +90,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
{
withLinks && (
<div id="socials">
<div id="socials" class="when-no-print">
<a
href="/discord"
class="pressable-icon"
@ -236,14 +236,16 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
gap: 3em;
}
&.with-links {
border-left: 0.1em solid var(--color-base-1000);
grid-template-areas: "license" "socials";
@media screen {
&.with-links {
border-left: 0.1em solid var(--color-base-1000);
grid-template-areas: "license" "socials";
@media (max-width: 35rem) {
grid-template-areas: "socials" "license";
border-left: unset;
padding-left: unset;
@media (max-width: 35rem) {
grid-template-areas: "socials" "license";
border-left: unset;
padding-left: unset;
}
}
}

View File

@ -5,10 +5,10 @@ import "@fontsource-variable/vollkorn";
import "@fontsource-variable/murecho";
interface Props {
title: string;
title?: string | undefined;
}
const { title } = Astro.props;
const { title = "Accords Library" } = Astro.props;
const userAgent = Astro.request.headers.get("user-agent") ?? "";
const parser = new UAParser(userAgent);
@ -25,6 +25,7 @@ const { currentTheme } = Astro.locals;
"manual-theme": currentTheme !== "auto",
"light-theme": currentTheme === "light",
"dark-theme": currentTheme === "dark",
"texture-dots": !isIOS,
}}
>
<head>
@ -33,6 +34,17 @@ const { currentTheme } = Astro.locals;
<title>{title}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="stylesheet" href="/css/tippy.css" />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="#fdebd4"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#27231e"
/>
<link rel="manifest" href="/site.webmanifest" />
<noscript>
<style>
@ -47,7 +59,7 @@ const { currentTheme } = Astro.locals;
<ViewTransitions />
</head>
<body class:list={{ "texture-dots": !isIOS }}>
<body>
<slot />
</body>
</html>
@ -57,154 +69,63 @@ const { currentTheme } = Astro.locals;
}
<style is:global>
@media print {
.when-no-print {
display: none !important;
}
}
html {
&.light-theme {
--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-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-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);
--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;
--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;
}
& .when-light-theme {
display: initial !important;
}
&.dark-theme {
--color-base-1000: #ebeae7;
--color-base-950: #eae5e0;
--color-base-900: #e8dfd8;
--color-base-850: #e4d1c4;
--color-base-800: #e0bfaa;
--color-base-750: #dcb095;
--color-base-700: #d4a07f;
--color-base-650: #cb916c;
--color-base-600: #bf835d;
--color-base-550: #b07751;
--color-base-500: #a06b48;
--color-base-450: #8f5f40;
--color-base-400: #7d5539;
--color-base-350: #6b4a33;
--color-base-300: #5c412e;
--color-base-250: #4a3728;
--color-base-200: #3a2d22;
--color-base-175: #312820;
--color-base-150: #27231e;
--color-base-100: #1c1b16;
--color-base-50: #11110d;
--color-base-0: #000000;
--color-elevation-2: var(--color-base-200);
--color-elevation-1: var(--color-base-175);
--color-elevation-0: var(--color-base-150);
--color-shadow: var(--color-base-0);
--color-shadow-1: var(--color-base-0);
--color-shadow-2: var(--color-base-50);
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
& .when-light-theme {
display: none !important;
}
& .when-dark-theme {
display: initial !important;
}
& .when-dark-theme {
display: none !important;
}
&: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;
font-weight: 430;
--color-elevation-2: var(--color-base-100);
--color-elevation-1: var(--color-base-125);
--color-elevation-0: var(--color-base-150);
strong {
font-weight: 600;
}
--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 screen {
&.dark-theme {
--color-base-1000: #ebeae7;
--color-base-950: #eae5e0;
--color-base-900: #e8dfd8;
@ -247,25 +168,134 @@ const { currentTheme } = Astro.locals;
display: initial !important;
}
}
&: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;
--color-base-900: #e8dfd8;
--color-base-850: #e4d1c4;
--color-base-800: #e0bfaa;
--color-base-750: #dcb095;
--color-base-700: #d4a07f;
--color-base-650: #cb916c;
--color-base-600: #bf835d;
--color-base-550: #b07751;
--color-base-500: #a06b48;
--color-base-450: #8f5f40;
--color-base-400: #7d5539;
--color-base-350: #6b4a33;
--color-base-300: #5c412e;
--color-base-250: #4a3728;
--color-base-200: #3a2d22;
--color-base-175: #312820;
--color-base-150: #27231e;
--color-base-100: #1c1b16;
--color-base-50: #11110d;
--color-base-0: #000000;
--color-elevation-2: var(--color-base-200);
--color-elevation-1: var(--color-base-175);
--color-elevation-0: var(--color-base-150);
--color-shadow: var(--color-base-0);
--color-shadow-1: var(--color-base-0);
--color-shadow-2: var(--color-base-50);
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
& .when-light-theme {
display: none !important;
}
& .when-dark-theme {
display: initial !important;
}
}
}
}
}
/* THEMING */
html,
body {
padding: 0;
margin: 0;
*::selection {
color: var(--color-elevation-0);
background: var(--color-base-600);
}
@media screen {
.high-contrast-text {
text-shadow: 0 0 0.6em var(--color-elevation-0);
}
}
html {
color: var(--color-base-1000);
background-color: var(--color-base-150);
@media screen {
background-color: var(--color-base-150);
}
}
body {
padding: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
margin: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
min-height: 100vb;
box-sizing: border-box;
display: flex;
flex-direction: column;
background-color: transparent;
}
h1,
@ -275,7 +305,8 @@ const { currentTheme } = Astro.locals;
h5,
h6,
p,
button {
button,
html {
padding: 0;
margin: 0;
}
@ -293,6 +324,10 @@ const { currentTheme } = Astro.locals;
--font-serif: "Vollkorn Variable", serif;
}
button {
background-color: unset;
}
button,
body {
font-family: "Murecho Variable", sans-serif;
@ -311,12 +346,14 @@ const { currentTheme } = Astro.locals;
}
}
.texture-dots {
background-size: 10cm;
background-attachment: local;
background-image: var(--texture-dots);
background-blend-mode: var(--texture-dots-blend);
background-repeat: repeat;
@media screen {
.texture-dots {
background-size: 10cm;
background-attachment: local;
background-image: var(--texture-dots);
background-blend-mode: var(--texture-dots-blend);
background-repeat: repeat;
}
}
.pressable-icon {
@ -332,21 +369,21 @@ const { currentTheme } = Astro.locals;
}
}
.keycap {
.pressable {
--foreground-color: var(--color-base-650);
color: var(--foreground-color);
border: 0.1rem solid var(--foreground-color);
background-color: var(--color-elevation-0);
backdrop-filter: blur(10px);
transition-duration: 250ms;
transition-property: padding-top, box-shadow, background-color, color,
border-color;
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
border-color, translate;
&:hover {
--foreground-color: var(--color-base-1000);
box-shadow: 0 2px 2px var(--color-shadow-2);
background-color: var(--color-elevation-1);
translate: 0 -2px;
}
&:active {
@ -354,6 +391,7 @@ const { currentTheme } = Astro.locals;
--foreground-color: var(--color-base-1000);
background-color: var(--color-elevation-2);
box-shadow: 0 6px 12px 2px var(--color-shadow-2);
translate: unset;
}
}
@ -368,4 +406,50 @@ const { currentTheme } = Astro.locals;
.when-no-js {
display: none;
}
.prose {
font-size: 16px;
line-height: 1.75;
max-width: 35rem;
> *:first-child {
margin-top: unset;
padding-top: unset;
}
> *:last-child {
margin-bottom: unset;
padding-bottom: unset;
}
> p {
margin-top: 1.25em;
margin-bottom: 1.25em;
}
> h2 {
font-size: 32px;
}
> h3 {
font-size: 24px;
}
> h2,
> h3,
> h4,
> h5,
> h6 {
margin-top: 2em;
margin-bottom: 1em;
line-height: 1;
}
> h2 + h3,
> h3 + h4,
> h4 + h5,
> h5 + h6 {
margin-top: -0.75em;
}
}
</style>

View File

@ -12,7 +12,7 @@ interface Props {
const { withTitle, class: className } = Astro.props;
const { currentLocale } = Astro.locals;
const { t } = await getI18n(currentLocale);
const { t, formatLocale } = await getI18n(currentLocale);
---
{
@ -22,13 +22,13 @@ const { t } = await getI18n(currentLocale);
<Tooltip trigger="click" class={className}>
<div id="content" slot="tooltip-content">
{
cache.locales.map(id => (
cache.locales.map(({ id }) => (
<a
class:list={{ current: currentLocale === id }}
href={`?action-lang=${id}`}
data-astro-prefetch="tap"
>
{id}
{formatLocale(id)}
</a>
))
}

View File

@ -5,45 +5,49 @@ 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 { getI18n } from "translations/translations";
import Tooltip from "components/Tooltip.astro";
interface Props {
breadcrumb: { name: string; slug: string }[];
parentPages?: { name: string; slug: string; type: string }[] | undefined;
hideHomeButton?: boolean;
}
const { breadcrumb } = Astro.props;
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { parentPages = [], hideHomeButton = false } = Astro.props;
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<nav id="topbar">
<nav id="topbar" class="when-no-print">
{
breadcrumb.length > 0 && (
<div id="breadcrumb" class="hide-scrollbar">
(!hideHomeButton || parentPages.length > 0) && (
<div id="breadcrumb" class="hide-scrollbar high-contrast-text">
<a href="/">
<>
<Icon name="accords" width={16} height={16} />
<p>{t("home.title")}</p>
</>
<Icon name="material-symbols:home" width={16} height={16} />
<p>{t("home.title")}</p>
</a>
{breadcrumb.map(({ name, slug }) => (
<>
<Icon
name="material-symbols:arrow-forward-ios"
width={12}
height={12}
/>
<a href={slug}>
<p>{name}</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>
)}
</div>
)
}
<div id="toolbar">
<a href={getLocalizedUrl("/search")}>
<Button
@ -89,19 +93,25 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
display: flex;
place-items: center;
overflow-x: scroll;
gap: 8px;
margin-left: -0.8em;
& > svg {
flex-shrink: 0;
}
& > a {
& > a,
& > :global(tippy-tooltip > div) {
text-decoration: none;
flex-shrink: 0;
display: flex;
place-items: center;
gap: 0.4em;
padding: 0.4em 0.6em;
padding: 0.7em 0.8em;
border-radius: 9999px;
cursor: pointer;
backdrop-filter: blur(10px);
transition: 150ms background-color;
@ -112,10 +122,6 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
&:active {
background-color: var(--color-base-300);
}
&:last-child {
text-decoration: underline;
}
}
}

View File

@ -21,7 +21,7 @@ const icons =
<button
id={id}
class:list={[{ "with-title": !!title }, className]}
class:list={["pressable", "high-contrast-text", { "with-title": !!title }, className]}
aria-label={ariaLabel}
title={ariaLabel}
>
@ -35,15 +35,13 @@ const icons =
<style>
button {
--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: 9999px;
padding-left: 1em;
padding-right: 1em;
height: 2.5em;
backdrop-filter: blur(10px);
display: flex;
place-items: center;
place-content: center;
@ -56,7 +54,6 @@ const icons =
transition-duration: 250ms;
transition-property: padding-top, box-shadow, background-color, color,
border-color;
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
&.with-title > svg {
width: 1.2em;
@ -69,16 +66,12 @@ const icons =
}
&:hover {
--foreground-color: var(--color-base-1000);
box-shadow: inset 0 0.1em 0.1em 0 var(--color-shadow-2);
translate: unset;
}
&:active {
transition-duration: 75ms;
--foreground-color: var(--color-base-1000);
background-color: var(--color-elevation-2);
box-shadow: inset 0 0.1em 0.1em 0.1em var(--color-shadow-2);
padding-top: 0.2em;
}
}

View File

@ -0,0 +1,31 @@
---
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

@ -1,21 +1,11 @@
---
interface Props {
id?: string;
class?: string;
}
const { class:className, id } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<maso-target class={className} id={id}>
<maso-target>
<slot />
</maso-target>
{
/* ------------------------------------------- JS --------------------------------------------- */
}

View File

@ -1,41 +1,26 @@
---
import type { RichTextContent } from "src/shared/payload/payload-sdk";
import RTNode from "./components/RTNode.astro";
import RTProse from "./components/RTProse.astro";
import { type RichTextContext, defaultContext } from "src/utils/richText";
import ConditionalWrapper from "components/ConditionalWrapper.astro";
interface Props {
content: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
};
};
content: RichTextContent;
context?: RichTextContext;
}
const { content } = Astro.props;
const { content, context = defaultContext } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div class="rich-text">
{content.root.children.map((node) => <RTNode node={node} />)}
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
.rich-text {
& li[checkbox]::marker {
content: "☐";
}
& li[checkbox][checked]::marker {
content: "☒";
}
<ConditionalWrapper condition={context.depth === 1} wrapper={RTProse}>
{
content.root.children.map((node) => (
<RTNode node={node} context={context} />
))
}
</style>
</ConditionalWrapper>

View File

@ -0,0 +1,42 @@
---
import type { RichTextContext } from "src/utils/richText";
import RTSection from "./components/RTSection.astro";
import RTTranscript from "./components/RTTranscript.astro";
interface Props {
node: {
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
};
};
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} />
) : (
<p>{`Unknown block type: ${node.fields.blockType}. Please contact website technical administrator.`}</p>
)
}

View File

@ -0,0 +1,27 @@
---
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

@ -0,0 +1,41 @@
---
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

@ -0,0 +1,43 @@
---
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 {
node: {
type: string;
version: number;
format: number;
text: string;
[k: string]: unknown;
fields: {
id: string;
blockName: string;
blockType: string;
lines: RichTextContent;
};
};
context: RichTextContext;
}
const { node, context } = Astro.props;
---
{
context.depth < 2 ? (
<h2>{node.fields.blockName}</h2>
) : context.depth === 2 ? (
<h3>{node.fields.blockName}</h3>
) : context.depth === 3 ? (
<h4>{node.fields.blockName}</h4>
) : context.depth === 4 ? (
<h5>{node.fields.blockName}</h5>
) : (
<h6>{node.fields.blockName}</h6>
)
}
<RichText
content={node.fields.lines}
context={{ ...context, depth: context.depth + 1 }}
/>

View File

@ -0,0 +1,52 @@
---
import type { RichTextContext } from "src/utils/richText";
import RTLine from "./RTLine.astro";
import RTCue from "./RTCue.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 }[];
};
};
context: RichTextContext;
}
const { node, context } = 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>
);
}
})
}
</div>
<style>
div {
padding-block: 1em;
display: grid;
grid-template-columns: auto 1fr;
gap: 1.5em 2em;
}
</style>

View File

@ -1,19 +0,0 @@
---
type BasicNode = {
type: string;
version: number;
[k: string]: unknown;
};
interface Props {
node: BasicNode;
}
const { node } = Astro.props;
---
<p>
{
`Unknown node type: ${node.type}. Please contact website technical administrator.`
}
</p>

View File

@ -1,5 +1,5 @@
---
import RTError from "../RTError.astro";
import type { RichTextContext } from "src/utils/richText";
import RTNode from "../RTNode.astro";
import RTCustomLink from "./components/RTCustomLink.astro";
import RTInternalLink from "./components/RTInternalLink.astro";
@ -21,25 +21,26 @@ interface Props {
};
[k: string]: unknown;
};
context: RichTextContext;
}
const { node } = Astro.props;
const { node, context } = Astro.props;
---
{
node.fields.linkType === "custom" ? (
<RTCustomLink href={node.fields.url} newTab={node.fields.newTab}>
{node.children.map((node) => (
<RTNode node={node} />
<RTNode node={node} context={context} />
))}
</RTCustomLink>
) : node.fields.linkType === "internal" ? (
<RTInternalLink doc={node.fields.doc}>
{node.children.map((node) => (
<RTNode node={node} />
<RTNode node={node} context={context} />
))}
</RTInternalLink>
) : (
<RTError node={node} />
<p>{`Unknown link type: ${node.fields.linkType}. Please contact website technical administrator.`}</p>
)
}

View File

@ -1,4 +1,5 @@
---
import type { RichTextContext } from "src/utils/richText";
import RTBasicListItem from "./components/RTBasicListItem.astro";
import RTCheckListItem from "./components/RTCheckListItem.astro";
@ -6,38 +7,42 @@ interface Props {
node: {
type: string;
version: number;
format: number;
text: string;
listType: string;
children: {
type: string;
version: number;
[k: string]: unknown;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
}[];
[k: string]: unknown;
};
context: RichTextContext;
}
const { node } = Astro.props;
const { node, context } = Astro.props;
---
{
node.listType === "number" ? (
<ol>
{node.children.map((node) => (
<RTBasicListItem node={node} />
<RTBasicListItem node={node} context={context} />
))}
</ol>
) : node.listType === "bullet" ? (
<ul>
{node.children.map((node) => (
<RTBasicListItem node={node} />
<RTBasicListItem node={node} context={context} />
))}
</ul>
) : node.listType === "check" ? (
<ul>
{node.children.map((node) => (
<RTCheckListItem node={node} />
<RTCheckListItem node={node} context={context} />
))}
</ul>
) : (

View File

@ -1,4 +1,5 @@
---
import type { RichTextContext } from "src/utils/richText";
import RTNode from "../../RTNode.astro";
interface Props {
@ -10,14 +11,12 @@ interface Props {
}[];
type: string;
version: number;
format: number;
text: string;
listType: string;
[k: string]: unknown;
};
context: RichTextContext;
}
const { node } = Astro.props;
const { node, context } = Astro.props;
---
<li>{node.children.map((node) => <RTNode node={node} />)}</li>
<li>{node.children.map((node) => <RTNode node={node} context={context} />)}</li>

View File

@ -1,6 +1,7 @@
---
import { Icon } from "astro-icon/components";
import RTNode from "../../RTNode.astro";
import type { RichTextContext } from "src/utils/richText";
interface Props {
node: {
@ -11,14 +12,12 @@ interface Props {
}[];
type: string;
version: number;
format: number;
text: string;
listType: string;
[k: string]: unknown;
};
context: RichTextContext;
}
const { node } = Astro.props;
const { node, context } = Astro.props;
---
<li>
@ -27,14 +26,14 @@ const { node } = Astro.props;
? "material-symbols:check-box"
: "material-symbols:check-box-outline-blank"}
/>
{node.children.map((node) => <RTNode node={node} />)}
{node.children.map((node) => <RTNode node={node} context={context} />)}
</li>
<style>
li {
&::marker {
content: ""
content: "";
}
margin-left: -16px;
}
</style>
</style>

View File

@ -1,9 +1,10 @@
---
import RTParagraph from "./RTParagraph.astro";
import RTList from "./RTList/RTList.astro";
import RTError from "./RTError.astro";
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";
interface Props {
node: {
@ -11,9 +12,10 @@ interface Props {
version: number;
[k: string]: unknown;
};
context: RichTextContext;
}
const { node } = Astro.props;
const { node, context } = Astro.props;
let NodeElement;
switch (node.type) {
@ -33,10 +35,16 @@ switch (node.type) {
NodeElement = RTLink;
break;
default:
NodeElement = RTError;
case "block":
NodeElement = RTBlock;
break;
}
---
<NodeElement node={node} />
{
NodeElement ? (
<NodeElement node={node} context={context} />
) : (
<p>{`Unknown node type: ${node.type}. Please contact website technical administrator.`}</p>
)
}

View File

@ -1,4 +1,5 @@
---
import type { RichTextContext } from "src/utils/richText";
import RTNode from "./RTNode.astro";
interface Props {
@ -12,23 +13,19 @@ interface Props {
}[];
[k: string]: unknown;
};
context: RichTextContext;
}
const { node } = Astro.props;
const { node, context } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<p>{node.children.map((node) => <RTNode node={node} />)}</p>
<p>{node.children.map((node) => <RTNode node={node} context={context} />)}</p>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
p {
margin-bottom: 1em;
}
</style>

View File

@ -0,0 +1,3 @@
<div class="prose">
<slot />
</div>

View File

@ -0,0 +1,94 @@
---
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

@ -0,0 +1,62 @@
---
import { Icon } from "astro-icon/components";
interface Props {
icon: string;
title: string;
values: string[];
}
const { icon, title, values } = Astro.props;
if (values.length === 0) return;
---
<div id="container">
<div id="title">
<Icon name={icon} width={24} height={24} />
<p>{title}</p>
</div>
<div id="values">
{values.map((value) => <div class="pill">{value}</div>)}
</div>
</div>
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: center;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
translate: 0px -0.1em;
}
}
& > #values {
display: flex;
flex-wrap: wrap;
gap: 6px;
& > .pill {
border: 1px solid var(--color-base-1000);
border-radius: 9999px;
padding-top: 0.15em;
padding-bottom: 0.25em;
padding-inline: 0.6em;
}
}
}
</style>

View File

@ -0,0 +1,216 @@
---
import RichText from "components/RichText/RichText.astro";
import { payload } 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";
export const partial = true;
interface Props {
lang?: string;
slug?: string;
}
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 { getLocalizedUrl, formatCategory, formatContentType } = 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: "",
});
---
<MasoTarget>
{content.thumbnail && <AppLayoutBackgroundImg src={content.thumbnail.url} />}
<div id="layout">
<div id="left">
<AppLayoutTitle
title={translation.title}
pretitle={translation.pretitle}
subtitle={translation.subtitle}
/>
{
content.thumbnail && (
<img
id="thumbnail"
class="when-not-large"
src={content.thumbnail.url}
width={content.thumbnail.width}
height={content.thumbnail.height}
/>
)
}
{
translation.summary && (
<div id="summary" class="high-contrast-text">
<RichText content={translation.summary} />
</div>
)
}
{
(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">
<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}
/>
</div>
{
translation.format.text && (
<>
<hr />
<div id="text">
<RichText content={translation.format.text.content} />
</div>
</>
)
}
</div>
<div id="right" class="when-large">
{
content.thumbnail && (
<img
id="thumbnail"
src={content.thumbnail.url}
width={content.thumbnail.width}
height={content.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}
/>
</div>
</div>
</div>
</MasoTarget>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#layout {
display: grid;
justify-content: space-between;
container-type: inline-size;
@media (min-width: 80rem) {
grid-template-columns: 35rem 35rem;
}
& > #left {
& > #thumbnail {
width: 100%;
max-width: 35rem;
height: auto;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
margin-block: 2em;
}
& > #summary {
backdrop-filter: blur(5px);
padding: 1.5em;
margin: -1.5em;
margin-block: 1em;
border-radius: 3em;
}
hr {
border: none;
border-top: 3px dotted var(--color-base-500);
margin-block: 3em;
}
}
& > #right {
& > #thumbnail {
width: 100%;
height: auto;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
}
}
.meta-container {
@media (max-width: 35rem) {
margin-block: 5em;
gap: 2em;
}
margin-block: 2em;
display: grid;
gap: 1em;
}
.when-large {
@media (max-width: 80rem) {
display: none !important;
}
}
.when-not-large {
@media (min-width: 80rem) {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,14 @@
---
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

@ -1,18 +0,0 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import Content from "pages/api/content.astro";
const { currentLocale } = Astro.locals;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout
breadcrumb={[{ name: "About us", slug: "about" }]}
title="About us"
description="This is a page to test the Temporary Language Override™ feature"
>
<Content lang={currentLocale} />
</AppLayout>

View File

@ -6,7 +6,9 @@ import RichText from "components/RichText/RichText.astro";
import FoldersSection from "./_components/FoldersSection.astro";
const { slug } = Astro.params;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
Astro.locals.currentLocale
);
if (!slug) {
return Astro.redirect("/en/404");
@ -16,7 +18,7 @@ const folder = await payload.getFolder(slug);
const meta = getLocalizedMatch(folder.translations, { name: slug });
// TODO: handle folder not found
// TODO: handle rich text description
// TODO: send description as RichTextContent instead of string
// TODO: handle light and dark illustration for applayout
---
@ -25,7 +27,6 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
}
<AppLayout title={meta.name}>
{
meta.description && (
<div slot="header-description">
@ -33,26 +34,49 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
</div>
)
}
<div id="main">
{
folder.sections.type === "single" ? (
<FoldersSection folders={folder.sections.subfolders} />
) : (
<div id="sections">
{folder.sections.sections.map(({ subfolders, translations }) => (
<FoldersSection
folders={subfolders}
title={
getLocalizedMatch<{
language: string;
name: string | undefined;
}>(translations, { name: undefined }).name
}
/>
))}
</div>
)
}
{
folder.sections.type === "single" ? (
<FoldersSection folders={folder.sections.subfolders} />
) : (
<div id="sections">
{folder.sections.sections.map(({ subfolders, translations }) => (
<FoldersSection
folders={subfolders}
title={
getLocalizedMatch<{
language: string;
name: string | undefined;
}>(translations, { name: undefined }).name
}
/>
))}
</div>
)
}
<div>
{
folder.files.map(({ relationTo, value }) => {
if (relationTo === "contents") {
return (
<a
class="pressable"
href={getLocalizedUrl(`/contents/${value.slug}`)}
>
{value.slug}
</a>
);
}
return (
<a href={getLocalizedUrl(`/library-item/${value.slug}`)}>
{value.slug}
</a>
);
})
}
</div>
</div>
</AppLayout>
{
@ -60,8 +84,13 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
}
<style>
#sections {
#main {
display: grid;
gap: 2.5em;
gap: 4em;
#sections {
display: grid;
gap: 2.5em;
}
}
</style>

View File

@ -9,7 +9,7 @@ interface Props {
const { icon = "material-symbols:folder-outline", title, href } = Astro.props;
---
<a href={href} class="keycap">
<a href={href} class="pressable">
<Icon name={icon} />
<div id="right">
<h3>{title}</h3>

View File

@ -17,9 +17,9 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<AppLayout
title="Accords Library"
illustration="/img/bg-home.webp"
illustrationSize="60vw"
illustrationPosition="20%"
backgroundIllustration="/img/bg-home2.webp"
hideFooterLinks
hideHomeButton
>
<div id="title" slot="header-title">
<Icon name="accords" />
@ -29,7 +29,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
</div>
</div>
<div id="description" slot="header-description">
<p set:html={t("home.description")} />
<p set:html={t("home.description")} class="high-contrast-text" />
<a href={getLocalizedUrl("/about")}>
<Button
title={t("home.aboutUsButton")}
@ -39,7 +39,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
</div>
<div id="main">
<section id="library">
<section id="library" class="high-contrast-text">
<h2>{t("home.librarySection.title")}</h2>
<p set:html={t("home.librarySection.description")} />
<a href={getLocalizedUrl("/search")}>
@ -186,8 +186,17 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
flex-direction: column;
gap: 24px;
align-items: flex-start;
margin-bottom: 128px;
> p {
backdrop-filter: blur(5px);
padding: 1em;
margin: -1em;
border-radius: 5em;
}
@media (max-width: 35rem) {
align-items: center;
@ -267,7 +276,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
& > p {
max-width: 35em;
line-height: 1.4;
line-height: 1.5;
margin-top: 8px;
margin-bottom: 24px;
}

View File

@ -7,16 +7,13 @@ const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
const { t } = await getI18n(currentLocale);
---
<AppLayout
title={t("settings.title")}
breadcrumb={[{ name: t("settings.title"), slug: "settings" }]}
>
<AppLayout title={t("settings.title")}>
<div id="main">
<div class="section">
<h2>{t("settings.language.title")}</h2>
<p>{t("settings.language.description")}</p><br />
{
cache.locales.map((id) => (
cache.locales.map(({ id }) => (
<a
class:list={{ current: currentLocale === id }}
href={`?action-lang=${id}`}

View File

@ -8,7 +8,7 @@ interface Props {
const { img, name, href } = Astro.props;
---
<a href={href} aria-label={name} class="keycap">
<a href={href} aria-label={name} class="pressable">
{
img ? (
<>
@ -33,10 +33,10 @@ const { img, name, href } = Astro.props;
border-radius: 12px;
user-select: none;
aspect-ratio: 2;
& > img {
object-fit: contain;
aspect-ratio: 2;
height: 100%;
width: 100%;
}

View File

@ -9,7 +9,7 @@ interface Props {
const { pretitle, subtitle, title, href } = Astro.props;
---
<a href={href} class="keycap">
<a href={href} class="pressable">
<p class="pretitle">{pretitle}</p>
<h3>{title}</h3>
<p>{subtitle}</p>

View File

@ -10,7 +10,7 @@ interface Props {
const { icon, subtitle, title, href } = Astro.props;
---
<a href={href} class="keycap">
<a href={href} class="pressable">
<Icon name={icon} />
<div id="right">
<h3>{title}</h3>

View File

@ -1336,6 +1336,16 @@ export type EndpointFolder = {
};
lightThumbnail?: PayloadImage;
darkThumbnail?: PayloadImage;
files: (
| {
relationTo: "library-items";
value: LibraryItem;
}
| {
relationTo: "contents";
value: Content;
}
)[];
};
export type EndpointFolderPreview = {
@ -1350,6 +1360,59 @@ export type EndpointFolderPreview = {
darkThumbnail?: PayloadImage;
};
export type EndpointContent = {
slug: string;
thumbnail?: PayloadImage;
translations: {
language: string;
sourceLanguage: string;
pretitle?: string;
title: string;
subtitle?: string;
summary?: RichTextContent;
format: {
text?: {
content: RichTextContent;
toc: TableOfContentEntry[];
transcribers: string[];
translators: string[];
proofreaders: string[];
notes?: RichTextContent;
};
};
}[];
categories: string[];
type?: string;
};
export type EndpointRecorder = {
id: string;
username: string;
avatar?: PayloadImage;
languages: string[];
biographies: {
language: string;
biography: RichTextContent;
}[];
};
export type EndpointKey = {
id: string;
name: string;
type: Key["type"];
translations: {
language: string;
name: string;
short: string;
}[];
};
export type TableOfContentEntry = {
prefix: string;
title: string;
children: TableOfContentEntry[];
};
export type PayloadImage = {
url: string;
width: number;
@ -1358,6 +1421,22 @@ export type PayloadImage = {
filename: string;
};
export type RichTextContent = {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
export const payload = {
getWeapon: async (slug: string): Promise<EndpointWeapon> =>
await (await request(payloadApiUrl(Collections.Weapons, `slug/${slug}`))).json(),
@ -1371,4 +1450,10 @@ export const payload = {
await (await request(payloadApiUrl(Collections.Languages, `all`))).json(),
getCurrencies: async (): Promise<Currency[]> =>
await (await request(payloadApiUrl(Collections.Currencies, `all`))).json(),
getContent: async (slug: string): Promise<EndpointContent> =>
await (await request(payloadApiUrl(Collections.Contents, `slug/${slug}`))).json(),
getKeys: async (): Promise<EndpointKey[]> =>
await (await request(payloadApiUrl(Collections.Keys, `all`))).json(),
getRecorders: async (): Promise<EndpointRecorder[]> =>
await (await request(payloadApiUrl(Collections.Recorders, `all`))).json(),
};

View File

@ -1,18 +1,27 @@
import { payload } from "src/shared/payload/payload-sdk";
import {
payload,
type EndpointKey,
type EndpointRecorder,
type Language,
} from "src/shared/payload/payload-sdk";
type Cache = {
locales: string[];
locales: Language[];
currencies: string[];
keys: EndpointKey[];
recorders: EndpointRecorder[];
};
const fetchNewData = async (): Promise<Cache> => ({
locales: (await payload.getLanguages()).map(({ id }) => id),
locales: (await payload.getLanguages()),
currencies: (await payload.getCurrencies()).map(({ id }) => id),
keys: await payload.getKeys(),
recorders: await payload.getRecorders(),
});
export let cache = await fetchNewData();
setInterval(async () => {
console.log("Refreshing cached Payload data")
console.log("Refreshing cached Payload data");
cache = await fetchNewData();
}, 1000_000);

View File

@ -30,9 +30,7 @@ export const getCookieCurrency = (
cookies: AstroCookies
): string | undefined => {
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
return isValidCurrency(cookieValue)
? cookieValue
: undefined;
return isValidCurrency(cookieValue) ? cookieValue : undefined;
};
export const getCookieTheme = (
@ -53,4 +51,6 @@ export const isValidCurrency = (
export const isValidLocale = (
locale: string | null | undefined
): locale is string =>
locale !== null && locale != undefined && cache.locales.includes(locale);
locale !== null &&
locale != undefined &&
cache.locales.map(({ id }) => id).includes(locale);

18
src/utils/currencies.ts Normal file
View File

@ -0,0 +1,18 @@
import currencies from "src/shared/openExchange/currencies.json";
import { rates } from "src/shared/openExchange/rates.json";
type CurrencyCode = keyof typeof rates;
export const convert = (from: string, to: string, amount: number) => {
if (!isCurrencyCode(from)) return NaN;
if (!isCurrencyCode(to)) return NaN;
return (amount / rates[from]) * rates[to];
};
export const formatCurrency = (code: string) => {
if (!isCurrencyCode(code)) return code;
return currencies[code];
};
const isCurrencyCode = (code: string): code is CurrencyCode => code in rates;

1
src/utils/random.ts Normal file
View File

@ -0,0 +1 @@
export const getRandomId = () => Math.random().toString(36).substring(2);

5
src/utils/richText.ts Normal file
View File

@ -0,0 +1,5 @@
export type RichTextContext = {
depth: number;
};
export const defaultContext: RichTextContext = { depth: 1 };

View File

@ -4,6 +4,7 @@ import fr from "./fr.json";
import ja from "./ja.json";
import acceptLanguage from "accept-language";
import { KeysTypes } from "src/shared/payload/payload-sdk";
type WordingKeys = keyof typeof en;
const translationFiles: Record<string, Record<WordingKeys, string>> = {
@ -109,6 +110,39 @@ export const getI18n = async (locale: string) => {
return template;
};
const getLocalizedMatch = <T extends { language: string }>(
options: T[],
fallback: Omit<T, "language">
): Omit<T, "language"> & { language?: string } =>
options.find(({ language }) => language === locale) ??
options.find(({ language }) => language === defaultLocale) ?? {
...fallback,
};
const getLocalizedKey = (
keyType: KeysTypes,
keyId: string,
format: "short" | "default"
) => {
const category = cache.keys.find(
({ id, type }) => id === keyId && type === keyType
);
if (!category) {
return "UNKNOWN";
}
if (!category.translations) {
return category.name;
}
const translation = getLocalizedMatch(category.translations, {
name: category.name,
short: category.name,
});
return format === "default" ? translation.name : translation.short;
};
return {
t: (key: WordingKeys, values: Record<string, any> = {}): string => {
if (translations && key in translations) {
@ -117,13 +151,24 @@ export const getI18n = async (locale: string) => {
return `«${key}»`;
},
getLocalizedUrl: (url: string): string => `/${locale}${url}`,
getLocalizedMatch: <T extends { language: string }>(
options: T[],
fallback: Omit<T, "language">
): Omit<T, "language"> =>
options.find(({ language }) => language === locale) ??
options.find(({ language }) => language === defaultLocale) ??
fallback,
getLocalizedMatch,
formatCategory: (
id: string,
format: "short" | "default" = "default"
): string => getLocalizedKey(KeysTypes.Categories, id, format),
formatContentType: (id: string): string =>
getLocalizedKey(KeysTypes.Contents, id, "default"),
formatRecorder: (recorderId: string): string => {
const result = cache.recorders.find(({ id }) => id === recorderId);
if (!result) {
return "UNKNOWN";
}
return result.username;
},
formatLocale: (code: string): string =>
cache.locales.find(({ id }) => id === code)?.name ?? code,
};
};
@ -151,8 +196,8 @@ export const defaultLocale: Locale = "en";
export const getCurrentLocale = (pathname: string): Locale | undefined => {
for (const locale of cache.locales) {
if (pathname.startsWith(`/${locale}`)) {
return locale;
if (pathname.startsWith(`/${locale.id}`)) {
return locale.id;
}
}
return undefined;
@ -161,7 +206,7 @@ export const getCurrentLocale = (pathname: string): Locale | undefined => {
export const getBestAcceptedLanguage = (
request: Request
): Locale | undefined => {
acceptLanguage.languages(cache.locales);
acceptLanguage.languages(cache.locales.map(({ id }) => id));
return (
(acceptLanguage.get(