Tons more

This commit is contained in:
DrMint 2024-02-17 23:11:17 +01:00
parent 47fa0d93a1
commit 07f6375b46
41 changed files with 2230 additions and 569 deletions

View File

@ -1,31 +0,0 @@
Bun.serve({
port: 12498,
fetch: async (req) => {
const reqUrl = new URL(req.url);
const rewriteUrl = new URL(reqUrl);
rewriteUrl.hostname = "localhost";
rewriteUrl.port = "12499";
rewriteUrl.protocol = "http";
const rewrite = new Request(rewriteUrl, req);
const response = await fetch(rewrite, { redirect: "manual" });
console.log(`[${response.status}] ${rewriteUrl.pathname}`);
if (response.status === 404 && response.headers.has("Location")) {
// Prevent redirection from a non locale-specific page to the en locale-specific page
if (response.headers.get("location") === "/en" + rewriteUrl.pathname) {
rewriteUrl.pathname = "/en/" + rewriteUrl.pathname;
const rewrite = new Request(rewriteUrl, req);
return await fetch(rewrite, { redirect: "manual" });
}
return new Response(await response.blob(), {
headers: response.headers,
status: 302,
statusText: "Found",
});
}
return response;
},
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
import { writeFileSync } from "fs";
const PAYLOAD_FOLDER = `${process.cwd()}/src/shared/payload`;
const sdk = await fetch(`${import.meta.env.PAYLOAD_API_URL}/sdk`);
if (!sdk.ok) {
console.error("Failed to get the sdk", sdk.status, sdk.statusText);
} else {
const sdkFile = await sdk.text();
writeFileSync(`${PAYLOAD_FOLDER}/payload-sdk.ts`, sdkFile, {
encoding: "utf-8",
});
}

View File

@ -5,8 +5,8 @@ import Footer from "./components/Footer.astro";
interface Props { interface Props {
breadcrumb?: { name: string; slug: string }[]; breadcrumb?: { name: string; slug: string }[];
title?: string; title?: string | undefined;
description?: string; description?: string | undefined;
illustration?: string; illustration?: string;
illustrationSize?: string; illustrationSize?: string;
illustrationPosition?: string; illustrationPosition?: string;
@ -34,14 +34,17 @@ const {
<slot name="header-title"> <slot name="header-title">
<h1>{title}</h1> <h1>{title}</h1>
</slot> </slot>
<div id="description">
<slot name="header-description"> <slot name="header-description">
<p>{description}</p> <p>{description}</p>
</slot> </slot>
</div> </div>
<div id="image-container"></div> </div>
{illustration && <div id="image-container" />}
</div> </div>
</header> </header>
<main><slot name="main" /></main> <main><slot /></main>
<Footer withLinks={breadcrumb.length > 0} /> <Footer withLinks={breadcrumb.length > 0} />
</Html> </Html>
@ -78,8 +81,12 @@ const {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
& > p { & > #description {
max-width: 35em; max-width: 35em;
& > p {
line-height: 1.4;
}
} }
} }

View File

@ -1,6 +1,7 @@
--- ---
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro"; import Tooltip from "components/Tooltip.astro";
import { cache } from "src/utils/cachedPayload";
import { getI18n } from "translations/translations"; import { getI18n } from "translations/translations";
interface Props { interface Props {
@ -20,14 +21,17 @@ const { currentCurrency } = Astro.locals;
<Tooltip trigger="click" class={className}> <Tooltip trigger="click" class={className}>
<div id="content" slot="tooltip-content"> <div id="content" slot="tooltip-content">
{
cache.currencies.map((id) => (
<a <a
class:list={{ current: currentCurrency === "usd" }} class:list={{ current: currentCurrency === id }}
href="?action-currency=usd">USD</a href={`?action-currency=${id}`}
> data-astro-prefetch="tap"
<a
class:list={{ current: currentCurrency === "eur" }}
href="?action-currency=eur">EUR</a
> >
{id}
</a>
))
}
</div> </div>
<Button <Button
icon="material-symbols:currency-exchange" icon="material-symbols:currency-exchange"

View File

@ -26,6 +26,10 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
)}`; )}`;
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<footer> <footer>
{ {
withLinks && ( withLinks && (
@ -36,6 +40,18 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
<Icon name="accords" /> <Icon name="accords" />
<p>{t("footer.links.home.title")}</p> <p>{t("footer.links.home.title")}</p>
</a> </a>
<a href="/archives">
<Icon name="material-symbols:browse-outline" />
<p>{"Contents"}</p>
</a>
<a href="/chronicles">
<Icon name="material-symbols:book-2-outline" />
<p>{"Chronicles"}</p>
</a>
<a href="/changelog">
<Icon name="material-symbols:history" />
<p>{"Changelog"}</p>
</a>
<a href="/timeline"> <a href="/timeline">
<Icon name="material-symbols:calendar-month-outline" /> <Icon name="material-symbols:calendar-month-outline" />
<p>{t("footer.links.timeline.title")}</p> <p>{t("footer.links.timeline.title")}</p>
@ -126,10 +142,17 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
& > div { & > div {
max-width: 20em; max-width: 20em;
flex: 1;
@media (max-width: 35rem) {
max-width: unset;
}
} }
& > #nav { & > #nav {
display: grid; display: grid;
place-content: start;
flex: 1.5;
@media (max-width: 35rem) { @media (max-width: 35rem) {
place-items: center; place-items: center;
@ -155,8 +178,6 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
margin-left: -0.6em; margin-left: -0.6em;
@media (max-width: 65rem) { @media (max-width: 65rem) {
grid-template-columns: 1fr;
margin-top: 0.5em;
gap: unset; gap: unset;
} }

View File

@ -333,35 +333,27 @@ const { currentTheme } = Astro.locals;
} }
.keycap { .keycap {
transition-duration: 150ms; --foreground-color: var(--color-base-650);
transition-property: translate, box-shadow, background-color; color: var(--foreground-color);
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); border: 0.1rem solid var(--foreground-color);
background-color: var(--color-elevation-0); background-color: var(--color-elevation-0);
box-shadow: transition-duration: 250ms;
0 7px 15px -5px var(--color-shadow), transition-property: padding-top, box-shadow, background-color, color,
inset 0 0 50px var(--color-elevation-2), border-color;
inset 0 -7px 1px 5px var(--color-shadow-2); transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover { &:hover {
box-shadow: --foreground-color: var(--color-base-1000);
0 7px 15px -2px var(--color-shadow), box-shadow: 0 2px 2px var(--color-shadow-2);
inset 0 0 50px var(--color-elevation-2),
inset 0 -7px 1px 5px var(--color-shadow-2);
background-color: var(--color-elevation-1); background-color: var(--color-elevation-1);
} }
&:active { &:active {
transition-duration: 75ms; transition-duration: 75ms;
translate: 0 5px; --foreground-color: var(--color-base-1000);
background-color: var(--color-elevation-2); background-color: var(--color-elevation-2);
box-shadow: 0 6px 12px 2px var(--color-shadow-2);
box-shadow:
0 3px 5px -2px var(--color-shadow),
inset 0 0 50px var(--color-elevation-2),
inset 0 -6px 1px 6px var(--color-shadow-1);
} }
} }
@ -377,11 +369,3 @@ const { currentTheme } = Astro.locals;
display: none; display: none;
} }
</style> </style>
<script>
import htmx from "htmx.org";
// On any page navigation, reprocess HTMX
document.addEventListener("astro:page-load", () => {
htmx.process(document.querySelector("main")!);
});
</script>

View File

@ -1,7 +1,8 @@
--- ---
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro"; import Tooltip from "components/Tooltip.astro";
import { getI18n, locales } from "translations/translations"; import { cache } from "src/utils/cachedPayload";
import { getI18n } from "translations/translations";
interface Props { interface Props {
withTitle?: boolean | undefined; withTitle?: boolean | undefined;
@ -21,13 +22,13 @@ const { t } = await getI18n(currentLocale);
<Tooltip trigger="click" class={className}> <Tooltip trigger="click" class={className}>
<div id="content" slot="tooltip-content"> <div id="content" slot="tooltip-content">
{ {
locales.map((locale) => ( cache.locales.map(id => (
<a <a
class:list={{ current: locale === currentLocale }} class:list={{ current: currentLocale === id }}
href={`?action-lang=${locale}`} href={`?action-lang=${id}`}
data-astro-prefetch="tap" data-astro-prefetch="tap"
> >
{locale.toString().toUpperCase()} {id}
</a> </a>
)) ))
} }

View File

@ -11,7 +11,8 @@ interface Props {
} }
const { breadcrumb } = Astro.props; const { breadcrumb } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale); const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
--- ---
{ {
@ -44,10 +45,12 @@ const { t } = await getI18n(Astro.locals.currentLocale);
) )
} }
<div id="toolbar"> <div id="toolbar">
<a href={getLocalizedUrl("/search")}>
<Button <Button
icon="material-symbols:search" icon="material-symbols:search"
ariaLabel={t("header.topbar.search.tooltip")} ariaLabel={t("header.topbar.search.tooltip")}
/> />
</a>
<div class="separator"></div> <div class="separator"></div>
<div class="when-no-js"> <div class="when-no-js">

View File

@ -0,0 +1,18 @@
---
interface Props {
wrapper: (props?: any) => any;
condition: boolean;
}
const { wrapper: Wrapper, condition } = Astro.props;
---
{
condition ? (
<Wrapper>
<slot />
</Wrapper>
) : (
<slot />
)
}

View File

@ -0,0 +1,52 @@
---
interface Props {
id?: string;
class?: string;
href: string;
method?: "get" | "post" | "delete" | "put";
}
const { href, method = "get", class: className, id } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<maso-actor href={href} method={method} class={className} id={id}>
<slot />
</maso-actor>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
maso-actor {
cursor: pointer;
}
</style>
{
/* ------------------------------------------- JS --------------------------------------------- */
}
<script>
import { customElement } from "src/utils/customElements";
customElement("maso-actor", (elem) => {
const href = elem.getAttribute("href");
const method = elem.getAttribute("method");
if (!href || !method) return;
elem.addEventListener("click", async () => {
const elementToReplace = elem.closest("maso-target");
if (!elementToReplace) return;
const result = await fetch(href);
if (!result.ok) return;
const newContent = await result.text();
elementToReplace.outerHTML = newContent;
});
});
</script>

View File

@ -0,0 +1,26 @@
---
interface Props {
id?: string;
class?: string;
}
const { class:className, id } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<maso-target class={className} id={id}>
<slot />
</maso-target>
{
/* ------------------------------------------- JS --------------------------------------------- */
}
<script>
class MasoTarget extends HTMLElement {}
customElements.define("maso-target", MasoTarget);
</script>

View File

@ -4,33 +4,32 @@ interface Props {
trigger?: string | undefined; trigger?: string | undefined;
} }
const { class: className, trigger = "mouseenter focus" } = Astro.props; const { class: className, trigger } = Astro.props;
--- ---
<tippy-tooltip class={className} data-tippy-trigger={trigger}> {
/* ------------------------------------------- HTML ------------------------------------------- */
}
<tippy-tooltip class={className} trigger={trigger}>
<template><slot name="tooltip-content" /></template> <template><slot name="tooltip-content" /></template>
<slot /> <slot />
</tippy-tooltip> </tippy-tooltip>
{
/* ------------------------------------------- JS --------------------------------------------- */
}
<script> <script>
import tippy from "tippy.js"; import tippy from "tippy.js";
import htmx from "htmx.org"; import { customElement } from "src/utils/customElements";
class TippyTooltip extends HTMLElement { customElement("tippy-tooltip", (elem) => {
constructor() { tippy(elem, {
super();
tippy(this, {
allowHTML: true, allowHTML: true,
content: (ref) => content: (ref) => ref.querySelector(":scope > template")?.innerHTML ?? "",
ref.querySelector(":scope > template")?.innerHTML ?? "",
interactive: true, interactive: true,
onMount: (instance) => { trigger: elem.getAttribute("trigger") ?? "mouseenter focus",
htmx.process(instance.popper); });
},
}); });
}
}
customElements.define("tippy-tooltip", TippyTooltip);
</script> </script>

4
src/env.d.ts vendored
View File

@ -4,8 +4,8 @@
declare namespace App { declare namespace App {
interface Locals { interface Locals {
currentLocale: import("translations/translations").Locale currentLocale: string
currentTheme: "dark" | "auto" | "light" currentTheme: "dark" | "auto" | "light"
currentCurrency: "usd" | "eur" currentCurrency: string
} }
} }

View File

@ -1,13 +1,18 @@
import { defineMiddleware, sequence } from "astro:middleware"; import { defineMiddleware, sequence } from "astro:middleware";
import { z } from "zod";
import { import {
defaultLocale, defaultLocale,
getCookiePreferredLocale,
getCurrentLocale, getCurrentLocale,
getPreferredLocale, getBestAcceptedLanguage,
} from "translations/translations"; } from "translations/translations";
import {
const cookieThemeSchema = z.enum(["dark", "light", "auto"]); CookieKeys,
getCookieCurrency,
getCookieLocale,
getCookieTheme,
isValidCurrency,
isValidLocale,
themeSchema,
} from "src/utils/cookies";
const getAbsoluteLocaleUrl = (locale: string, url: string) => const getAbsoluteLocaleUrl = (locale: string, url: string) =>
`/${locale}${url}`; `/${locale}${url}`;
@ -23,91 +28,74 @@ const redirect = (
}); });
}; };
const localeNegotiator = defineMiddleware( const localeAgnosticPaths = ["/api/"];
({ cookies, url, request }, next) => {
const currentLocale = getCurrentLocale(url.pathname);
const preferredLocale = getPreferredLocale(request);
if (url.pathname.startsWith("/api/")) { const localeNegotiator = defineMiddleware(({ cookies, url, request }, next) => {
if (localeAgnosticPaths.some((prefix) => url.pathname.startsWith(prefix))) {
return next(); return next();
} }
const cookiePreferredLocale = getCookiePreferredLocale(cookies); const currentLocale = getCurrentLocale(url.pathname);
const acceptedLocale = getBestAcceptedLanguage(request);
const cookieLocale = getCookieLocale(cookies);
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? defaultLocale;
if (!currentLocale) { if (!currentLocale) {
const redirectURL = getAbsoluteLocaleUrl( const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
cookiePreferredLocale ?? preferredLocale ?? defaultLocale,
url.pathname
);
return redirect(redirectURL); return redirect(redirectURL);
} }
if (cookiePreferredLocale) { if (currentLocale !== bestMatchingLocale) {
if (cookiePreferredLocale !== currentLocale) {
const pathnameWithoutLocale = url.pathname.substring( const pathnameWithoutLocale = url.pathname.substring(
currentLocale.length + 1 currentLocale.length + 1
); );
const redirectURL = getAbsoluteLocaleUrl( const redirectURL = getAbsoluteLocaleUrl(
cookiePreferredLocale, bestMatchingLocale,
pathnameWithoutLocale pathnameWithoutLocale
); );
return redirect(redirectURL); return redirect(redirectURL);
} }
} else if (preferredLocale) {
if (preferredLocale !== currentLocale) {
const pathnameWithoutLocale = url.pathname.substring(
currentLocale.length + 1
);
const redirectURL = getAbsoluteLocaleUrl(
preferredLocale,
pathnameWithoutLocale
);
return redirect(redirectURL);
}
}
return next(); return next();
} });
);
const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => { const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
// TODO: Verify locale typing
const actionLang = url.searchParams.get("action-lang"); const actionLang = url.searchParams.get("action-lang");
if (actionLang) { if (isValidLocale(actionLang)) {
const currentLocale = getCurrentLocale(url.pathname); const currentLocale = getCurrentLocale(url.pathname);
const pathnameWithoutLocale = currentLocale const pathnameWithoutLocale = currentLocale
? url.pathname.substring(currentLocale.length + 1) ? url.pathname.substring(currentLocale.length + 1)
: url.pathname; : url.pathname;
const redirectURL = getAbsoluteLocaleUrl(actionLang, pathnameWithoutLocale); const redirectURL = getAbsoluteLocaleUrl(actionLang, pathnameWithoutLocale);
return redirect(redirectURL, { return redirect(redirectURL, {
"Set-Cookie": `al_pref_languages=${JSON.stringify([actionLang])}; Path=/`, "Set-Cookie": `${CookieKeys.Languages}=${JSON.stringify([
actionLang,
])}; Path=/`,
}); });
} }
// TODO: Verify currency typing
const actionCurrency = url.searchParams.get("action-currency"); const actionCurrency = url.searchParams.get("action-currency");
if (actionCurrency) { if (isValidCurrency(actionCurrency)) {
return redirect(url.pathname, { return redirect(url.pathname, {
"Set-Cookie": `al_pref_currency=${JSON.stringify( "Set-Cookie": `${CookieKeys.Currency}=${JSON.stringify(
actionCurrency actionCurrency
)}; Path=/`, )}; Path=/`,
}); });
} }
const actionTheme = url.searchParams.get("action-theme"); const actionTheme = url.searchParams.get("action-theme");
const verifiedActionTheme = cookieThemeSchema.safeParse(actionTheme); const verifiedActionTheme = themeSchema.safeParse(actionTheme);
if (verifiedActionTheme.success) { if (verifiedActionTheme.success) {
url.searchParams.delete("action-theme"); url.searchParams.delete("action-theme");
if (verifiedActionTheme.data === "auto") { if (verifiedActionTheme.data === "auto") {
return redirect(url.pathname, { return redirect(url.pathname, {
"Set-Cookie": `al_pref_theme=; Path=/; Expires=${new Date( "Set-Cookie": `${CookieKeys.Theme}=; Path=/; Expires=${new Date(
0 0
).toUTCString()}`, ).toUTCString()}`,
}); });
} }
return redirect(url.pathname, { return redirect(url.pathname, {
"Set-Cookie": `al_pref_theme=${verifiedActionTheme.data}; Path=/`, "Set-Cookie": `${CookieKeys.Theme}=${verifiedActionTheme.data}; Path=/`,
}); });
} }
@ -126,12 +114,14 @@ const addContentLanguageResponseHeader = defineMiddleware(
} }
); );
const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies }, next) => { const provideLocalsToRequest = defineMiddleware(
async ({ url, locals, cookies }, next) => {
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en"; locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
locals.currentCurrency = cookies.get("al_pref_currency")?.value ?? "usd" locals.currentCurrency = getCookieCurrency(cookies) ?? "USD";
locals.currentTheme = cookies.get("al_pref_theme")?.value ?? "auto" locals.currentTheme = getCookieTheme(cookies) ?? "auto";
return next(); return next();
}); }
);
export const onRequest = sequence( export const onRequest = sequence(
addContentLanguageResponseHeader, addContentLanguageResponseHeader,

View File

@ -1 +1,9 @@
<h1>Oh nyo...</h1> ---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="Oh nyo..." />

View File

@ -0,0 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="WIP" />

View File

@ -0,0 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="WIP" />

View File

@ -0,0 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="WIP" />

View File

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

@ -0,0 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="WIP" />

View File

@ -1,95 +0,0 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import FolderCard from "./_components/FolderCard.astro";
import { getI18n } from "translations/translations";
const {getLocalizedUrl} = await getI18n(Astro.locals.currentLocale)
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout
breadcrumb={[
{ name: "Drakengard", slug: "drakengard" },
]}
title="Drakengard"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
illustration="/img/categories/light/dod.png"
>
<div id="main" slot="main">
<FolderCard
title="Games"
icon="material-symbols:stadia-controller-outline"
href={getLocalizedUrl("/drakengard/games")}
/>
<FolderCard
title="Guidebooks"
icon="material-symbols:menu-book-outline"
href={getLocalizedUrl("/drakengard/guidebooks")}
/>
<FolderCard
title="Manga"
icon="material-symbols:menu-book-outline"
href={getLocalizedUrl("/drakengard/manga")}
/>
<FolderCard
title="Novels"
icon="material-symbols:menu-book-outline"
href={getLocalizedUrl("/drakengard/novels")}
/>
<FolderCard
title="Novellas"
icon="material-symbols:menu-book-outline"
href={getLocalizedUrl("/drakengard/novellas")}
/>
<FolderCard
title="Characters"
icon="material-symbols:person-outline"
href={getLocalizedUrl("/drakengard/characters")}
/>
<FolderCard
title="Places"
icon="material-symbols:location-on-outline"
href={getLocalizedUrl("/drakengard/places")}
/>
<FolderCard
title="Enemies"
icon="material-symbols:sentiment-extremely-dissatisfied-outline"
href={getLocalizedUrl("/drakengard/enemies")}
/>
<FolderCard
title="Terms"
icon="material-symbols:indeterminate-question-box"
href={getLocalizedUrl("/drakengard/terms")}
/>
<FolderCard
title="Weapons"
icon="material-symbols:swords-outline"
href={getLocalizedUrl("/drakengard/weapons")}
/>
<FolderCard
title="Timeline"
icon="material-symbols:calendar-month-outline"
href={getLocalizedUrl("/drakengard/timeline")}
/>
<FolderCard
title="News"
icon="material-symbols:newspaper"
href={getLocalizedUrl("/drakengard/news")}
/>
</div>
</AppLayout>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#main {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 0.7rem 1rem;
}
</style>

View File

@ -0,0 +1,67 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
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";
const { slug } = Astro.params;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
if (!slug) {
return Astro.redirect("/en/404");
}
const folder = await payload.getFolder(slug);
const meta = getLocalizedMatch(folder.translations, { name: slug });
// TODO: handle folder not found
// TODO: handle rich text description
// TODO: handle light and dark illustration for applayout
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title={meta.name}>
{
meta.description && (
<div slot="header-description">
<RichText content={JSON.parse(meta.description)} />
</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>
)
}
</AppLayout>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#sections {
display: grid;
gap: 2.5em;
}
</style>

View File

@ -2,11 +2,11 @@
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
interface Props { interface Props {
title: string; title: string;
icon: string; icon?: string | undefined;
href: string; href: string;
} }
const { icon, title, href } = Astro.props; const { icon = "material-symbols:folder-outline", title, href } = Astro.props;
--- ---
<a href={href} class="keycap"> <a href={href} class="keycap">
@ -22,8 +22,7 @@ const { icon, title, href } = Astro.props;
place-items: center; place-items: center;
gap: 1em; gap: 1em;
color: var(--color-base-1000); color: var(--color-base-1000);
padding: 24px; padding: 12px 24px;
padding-top: 12px;
border-radius: 12px; border-radius: 12px;
text-decoration: none; text-decoration: none;

View File

@ -0,0 +1,46 @@
---
import type { EndpointFolderPreview } from "src/shared/payload/payload-sdk";
import FolderCard from "./FolderCard.astro";
import { getI18n } from "translations/translations";
interface Props {
title?: string | undefined;
folders: EndpointFolderPreview[];
}
const { title, folders } = Astro.props;
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
Astro.locals.currentLocale
);
---
<div>
{title && <h3>{title}</h3>}
<section>
{
folders.map(({ slug, translations, icon }) => (
<FolderCard
title={getLocalizedMatch(translations, { name: slug }).name}
icon={icon ? `material-symbols:${icon}` : undefined}
href={getLocalizedUrl(`/folders/${slug}`)}
/>
))
}
</section>
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
h3 {
margin-bottom: 1em;
}
section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 0.7rem 1rem;
}
</style>

View File

@ -3,9 +3,11 @@ import { Icon } from "astro-icon/components";
import AppLayout from "components/AppLayout/AppLayout.astro"; import AppLayout from "components/AppLayout/AppLayout.astro";
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import LinkCard from "../_components/LinkCard.astro"; import LinkCard from "../_components/LinkCard.astro";
import CategoryCard from "../_components/CategoryCard.astro";
import { getI18n } from "../../../translations/translations"; import { getI18n } from "../../../translations/translations";
import ChronicleCard from "pages/_components/ChronicleCard.astro";
import LibraryGrid from "pages/_components/LibraryGrid.astro";
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
--- ---
@ -36,120 +38,77 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
</a> </a>
</div> </div>
<Fragment slot="main">
<div id="main"> <div id="main">
<section id="library"> <section id="library">
<h2>{t("home.librarySection.title")}</h2> <h2>{t("home.librarySection.title")}</h2>
<p set:html={t("home.librarySection.description")} /> <p set:html={t("home.librarySection.description")} />
<a href={getLocalizedUrl("/search")}>
<Button
class="section-button"
title={t("home.librarySection.button")}
icon="material-symbols:browse-outline"
/>
</a>
<div class="grid"> <div class="grid">
<CategoryCard <LibraryGrid />
img={{
light: "/img/categories/light/dod.png",
dark: "/img/categories/dark/dod.png",
}}
name="Drakengard"
href={getLocalizedUrl("/drakengard")}
/>
<CategoryCard
img={{
light: "/img/categories/light/dod2.png",
dark: "/img/categories/dark/dod2.png",
}}
name="Drakengard 2"
href={getLocalizedUrl("/drakengard-2")}
/>
<CategoryCard
img={{
light: "/img/categories/light/dod3.png",
dark: "/img/categories/dark/dod3.png",
}}
name="Drakengard 3"
href={getLocalizedUrl("/drakengard-3")}
/>
<CategoryCard
img={{
light: "/img/categories/light/dod1.3.png",
dark: "/img/categories/dark/dod1.3.png",
}}
name="Drakengard 1.3"
href={getLocalizedUrl("/drakengard-1-3")}
/>
<CategoryCard
img={{
light: "/img/categories/light/nier.png",
dark: "/img/categories/dark/nier.png",
}}
name="NieR"
href={getLocalizedUrl("/nier")}
/>
<CategoryCard
img={{
light: "/img/categories/light/na.png",
dark: "/img/categories/dark/na.png",
}}
name="NieR:Automata"
href={getLocalizedUrl("/nier-automata")}
/>
<CategoryCard
img={{
light: "/img/categories/light/nier-rein.png",
dark: "/img/categories/dark/nier-rein.png",
}}
name="NieR Re[in]carnation"
href={getLocalizedUrl("/nier-rein")}
/>
<CategoryCard
img={{
light: "/img/categories/light/yorha.png",
dark: "/img/categories/dark/yorha.png",
}}
name="YoRHa"
href={getLocalizedUrl("/yorha")}
/>
<CategoryCard
img={{
light: "/img/categories/light/yorha-boys.png",
dark: "/img/categories/dark/yorha-boys.png",
}}
name="YoRHa Boys"
href={getLocalizedUrl("/yorha-boys")}
/>
<CategoryCard
img={{
light: "/img/categories/light/sino.png",
dark: "/img/categories/dark/sino.png",
}}
name="SINoALICE"
href={getLocalizedUrl("/sinoalice")}
/>
<CategoryCard
img={{
light: "/img/categories/light/404gamereset.png",
dark: "/img/categories/dark/404gamereset.png",
}}
name="404 Game Re:Set"
href={getLocalizedUrl("/404-game-reset")}
/>
<CategoryCard
img={{
light: "/img/categories/light/god-app.png",
dark: "/img/categories/dark/god-app.png",
}}
name="God App"
href={getLocalizedUrl("/god-app")}
/>
<CategoryCard
img={{
light: "/img/categories/light/voc.png",
dark: "/img/categories/dark/voc.png",
}}
name="Voice of Cards"
href={getLocalizedUrl("/voice-of-cards")}
/>
<CategoryCard name="Others..." href={getLocalizedUrl("/others")} />
</div> </div>
</section> </section>
<section>
<h2>{t("home.chroniclesSection.title")}</h2>
<p set:html={t("home.chroniclesSection.description")} />
<div class="flex">
<ChronicleCard
pretitle={"Preface"}
title={"The Yokoverse"}
subtitle={"Start reading the Chronicles here"}
href={getLocalizedUrl("/chronicles/preface")}
/>
<ChronicleCard
pretitle={"Chapter 1"}
title={"Drakengard 3"}
subtitle={"A cursed world, and the girl who hated it"}
href={getLocalizedUrl("/chronicles/drakengard-3")}
/>
<ChronicleCard
pretitle={"Chapter 2"}
title={"Drakengard"}
subtitle={"Humanity at its lowest"}
href={getLocalizedUrl("/chronicles/drakengard")}
/>
<ChronicleCard
pretitle={"Chapter 3"}
title={"NieR"}
subtitle={"The Glory of Mankind comes to an abrupt End"}
href={getLocalizedUrl("/chronicles/nier")}
/>
<ChronicleCard
pretitle={"Chapter 4"}
title={"NieR:Automata"}
subtitle={"Protagonists of Meaningless Stories"}
href={getLocalizedUrl("/chronicles/nier-automata")}
/>
<ChronicleCard
pretitle={"Epilogue"}
title={"Appendices"}
subtitle={"Protagonists of Meaningless Stories"}
href={getLocalizedUrl("/chronicles/nier-automata")}
/>
</div>
</section>
<section>
<h2>{t("home.changesSection.title")}</h2>
<p set:html={t("home.changesSection.description")} />
<a href={getLocalizedUrl("/changelog")}>
<Button
class="section-button"
title={t("home.changesSection.button")}
icon="material-symbols:history"
/>
</a>
</section>
<section> <section>
<h2>{t("home.moreSection.title")}</h2> <h2>{t("home.moreSection.title")}</h2>
<p set:html={t("home.moreSection.description")} /> <p set:html={t("home.moreSection.description")} />
@ -215,7 +174,6 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
</div> </div>
</section> </section>
</div> </div>
</Fragment>
</AppLayout> </AppLayout>
{ {
@ -230,11 +188,6 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
align-items: flex-start; align-items: flex-start;
margin-bottom: 128px; margin-bottom: 128px;
& > p {
line-height: 1.4;
max-width: 35em;
}
@media (max-width: 35rem) { @media (max-width: 35rem) {
align-items: center; align-items: center;
@ -315,8 +268,12 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
& > p { & > p {
max-width: 35em; max-width: 35em;
line-height: 1.4; line-height: 1.4;
padding-top: 8px; margin-top: 8px;
padding-bottom: 24px; margin-bottom: 24px;
}
& > a > :global(.section-button) {
margin-bottom: 24px;
} }
&#library { &#library {
@ -337,6 +294,18 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
column-gap: clamp(6px, 2vmin, 16px); column-gap: clamp(6px, 2vmin, 16px);
row-gap: clamp(6px + 6px, 2vmin + 6px, 16px); row-gap: clamp(6px + 6px, 2vmin + 6px, 16px);
} }
& > .flex {
display: flex;
column-gap: clamp(6px, 2vmin, 16px);
row-gap: clamp(6px + 6px, 2vmin + 6px, 16px);
flex-wrap: wrap;
@media (max-width: 35rem) {
display: grid;
grid-template-columns: 1fr;
}
}
} }
} }
</style> </style>

View File

@ -1,32 +0,0 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import Content from "pages/api/content.astro";
const { currentLocale } = Astro.locals;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout
breadcrumb={[{ name: "Drakengard", slug: "drakengard" }]}
title="Others"
description="This is a page to test the Temporary Language Override™ feature"
>
<div id="main" slot="main">
<Content lang={currentLocale} />
</div>
</AppLayout>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#main {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 0.7rem 1rem;
}
</style>

View File

@ -1,35 +1,36 @@
--- ---
import AppLayout from "components/AppLayout/AppLayout.astro"; import AppLayout from "components/AppLayout/AppLayout.astro";
import { getI18n, locales } from "translations/translations"; import { cache } from "src/utils/cachedPayload";
import { getI18n } from "translations/translations";
const { currentLocale, currentTheme, currentCurrency } = Astro.locals; const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
const { t } = await getI18n(currentLocale); const { t } = await getI18n(currentLocale);
--- ---
<AppLayout <AppLayout
title="Settings" title={t("settings.title")}
breadcrumb={[{ name: "Settings", slug: "settings" }]} breadcrumb={[{ name: t("settings.title"), slug: "settings" }]}
> >
<div id="main" slot="main"> <div id="main">
<div class="section"> <div class="section">
<h2>Language</h2> <h2>{t("settings.language.title")}</h2>
<p>{t("header.topbar.language.tooltip")}</p><br /> <p>{t("settings.language.description")}</p><br />
{ {
locales.map((locale) => ( cache.locales.map((id) => (
<a <a
class:list={{ current: locale === currentLocale }} class:list={{ current: currentLocale === id }}
href={`?action-lang=${locale}`} href={`?action-lang=${id}`}
data-astro-prefetch="tap" data-astro-prefetch="tap"
> >
{locale.toString().toUpperCase()} {id}
</a> </a>
)) ))
} }
</div> </div>
<div class="section"> <div class="section">
<h2>Theme</h2> <h2>{t("settings.theme.title")}</h2>
<p>{t("header.topbar.theme.tooltip")}</p><br /> <p>{t("settings.theme.description")}</p><br />
<a <a
class:list={{ current: currentTheme === "dark" }} class:list={{ current: currentTheme === "dark" }}
href="?action-theme=dark" href="?action-theme=dark"
@ -54,18 +55,19 @@ const { t } = await getI18n(currentLocale);
</div> </div>
<div class="section"> <div class="section">
<h2>Currency</h2> <h2>{t("settings.theme.title")}</h2>
<p>{t("header.topbar.currency.tooltip")}</p><br /> <p>{t("settings.theme.description")}</p><br />
{
cache.currencies.map((id) => (
<a <a
class:list={{ current: currentCurrency === "usd" }} class:list={{ current: currentCurrency === id }}
href="?action-currency=usd" href={`?action-currency=${id}`}
data-astro-prefetch="tap">USD</a data-astro-prefetch="tap"
>
<a
class:list={{ current: currentCurrency === "eur" }}
href="?action-currency=eur"
data-astro-prefetch="tap">EUR</a
> >
{id}
</a>
))
}
</div> </div>
</div> </div>
</AppLayout> </AppLayout>

View File

@ -0,0 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="WIP" />

View File

@ -0,0 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="WIP" />

View File

@ -1,6 +1,6 @@
--- ---
interface Props { interface Props {
img?: { light: string; dark: string }; img?: { light: string; dark: string } | undefined;
name: string; name: string;
href: string; href: string;
} }
@ -30,7 +30,6 @@ const { img, name, href } = Astro.props;
place-items: center; place-items: center;
padding: 24px; padding: 24px;
padding-bottom: 32px;
border-radius: 12px; border-radius: 12px;
user-select: none; user-select: none;

View File

@ -0,0 +1,52 @@
---
interface Props {
pretitle: string;
title: string;
subtitle: string;
href: string;
}
const { pretitle, subtitle, title, href } = Astro.props;
---
<a href={href} class="keycap">
<p class="pretitle">{pretitle}</p>
<h3>{title}</h3>
<p>{subtitle}</p>
</a>
<style>
a {
display: flex;
place-items: center;
gap: 1em;
color: var(--color-base-1000);
padding: 1.5em;
border-radius: 0.75em;
text-decoration: none;
& > svg {
width: clamp(1.5em, 6vw + 0.8em, 3em);
height: clamp(1.5em, 6vw + 0.8em, 3em);
flex-shrink: 0;
}
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.15em;
& > h3 {
font-size: 1.5em;
margin-bottom: 0.5em;
}
& > .pretitle {
font-size: 1em;
}
& > p {
font-size: 0.8em;
}
}
</style>

View File

@ -0,0 +1,24 @@
---
import { payload } from "src/shared/payload/payload-sdk";
import { getI18n } from "translations/translations";
import CategoryCard from "./CategoryCard.astro";
const folders = await payload.getRootFolders()
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
Astro.locals.currentLocale
);
---
{
folders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => (
<CategoryCard
img={
darkThumbnail && lightThumbnail
? { dark: darkThumbnail.url, light: lightThumbnail.url }
: undefined
}
name={getLocalizedMatch(translations, { name: slug }).name}
href={getLocalizedUrl(`/folders/${slug}`)}
/>
))
}

View File

@ -25,7 +25,6 @@ const { icon, subtitle, title, href } = Astro.props;
gap: 1em; gap: 1em;
color: var(--color-base-1000); color: var(--color-base-1000);
padding: 1.5em; padding: 1.5em;
padding-top: 0.75em;
border-radius: 0.75em; border-radius: 0.75em;
text-decoration: none; text-decoration: none;

View File

@ -1,7 +1,9 @@
--- ---
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro"; import Tooltip from "components/Tooltip.astro";
import { getI18n, locales } from "translations/translations"; import MasoActor from "components/Maso/MasoActor.astro";
import MasoTarget from "components/Maso/MasoTarget.astro";
import { getI18n } from "translations/translations";
export const partial = true; export const partial = true;
@ -15,7 +17,11 @@ const lang = Astro.props.lang ?? reqUrl.searchParams.get("lang")!;
const { t } = await getI18n(lang); const { t } = await getI18n(lang);
--- ---
<div class="hx-swap-content"> {
/* ------------------------------------------- HTML ------------------------------------------- */
}
<MasoTarget>
<Tooltip trigger="click" class="when-js"> <Tooltip trigger="click" class="when-js">
<Button <Button
icon="material-symbols:translate" icon="material-symbols:translate"
@ -25,33 +31,30 @@ const { t } = await getI18n(lang);
<div id="content" slot="tooltip-content"> <div id="content" slot="tooltip-content">
{ {
locales.map((locale) => ( ["en", "fr"].map((locale) => (
<a <MasoActor
class:list={{ current: locale === lang }} class:list={{ current: locale === lang }}
hx-get={`/api/content?lang=${locale}`} href={`/api/content?lang=${locale}`}
hx-trigger="click"
hx-target="closest .hx-swap-content"
hx-swap="outerHTML"
> >
{locale.toString().toUpperCase()} {locale.toString().toUpperCase()}
</a> </MasoActor>
)) ))
} }
</div> </div>
</Tooltip> </Tooltip>
<div set:html={t("home.description")} /> <div set:html={t("home.description")} />
</div> </MasoTarget>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style> <style>
#content { #content {
display: grid; display: grid;
gap: 0.5em; gap: 0.5em;
& > a { & > :global(.current) {
cursor: pointer;
}
& > .current {
color: var(--color-base-750); color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650); text-decoration: underline 0.08em var(--color-base-650);
} }

File diff suppressed because it is too large Load Diff

View File

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

56
src/utils/cookies.ts Normal file
View File

@ -0,0 +1,56 @@
import type { AstroCookies } from "astro";
import { cache } from "src/utils/cachedPayload";
import { z } from "zod";
export enum CookieKeys {
Currency = "al_pref_currency",
Theme = "al_pref_theme",
Languages = "al_pref_languages",
}
export const themeSchema = z.enum(["dark", "light", "auto"]);
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
const cookie = cookies.get(CookieKeys.Languages);
try {
const json = cookie?.json();
const result = z.array(z.string()).nonempty().safeParse(json);
if (result.success && isValidLocale(result.data[0])) {
return result.data[0];
}
} catch (e) {
console.error(e);
}
return undefined;
};
export const getCookieCurrency = (
cookies: AstroCookies
): string | undefined => {
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
return isValidCurrency(cookieValue)
? cookieValue
: undefined;
};
export const getCookieTheme = (
cookies: AstroCookies
): z.infer<typeof themeSchema> | undefined => {
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
const result = themeSchema.safeParse(cookieValue);
return result.success ? result.data : undefined;
};
export const isValidCurrency = (
currency: string | null | undefined
): currency is string =>
currency !== null &&
currency != undefined &&
cache.currencies.includes(currency);
export const isValidLocale = (
locale: string | null | undefined
): locale is string =>
locale !== null && locale != undefined && cache.locales.includes(locale);

View File

@ -0,0 +1,12 @@
export const customElement = (
name: string,
constructor?: (elem: HTMLElement) => void
) => {
class CustomElementClass extends HTMLElement {
constructor() {
super();
constructor?.(this);
}
}
customElements.define(name, CustomElementClass);
};

View File

@ -6,11 +6,25 @@
"home.aboutUsButton": "Read more about us", "home.aboutUsButton": "Read more about us",
"home.librarySection.title": "The Library", "home.librarySection.title": "The Library",
"home.librarySection.description": "Here you will find a list of IPs Yoko Taro worked on. Select one to discover all the media/content/articles that relates to this IP. <strong>Beware there can be spoilers.</strong>", "home.librarySection.description": "Here you will find a list of IPs Yoko Taro worked on. Select one to discover all the media/content/articles that relates to this IP. <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>",
"home.changesSection.title": "Whats new?",
"home.changesSection.description": "Here are the 10 most recently added/updated content. You can open the changelog to see all past changes.",
"home.changesSection.button": "Open the changelog",
"home.moreSection.title": "More content", "home.moreSection.title": "More content",
"home.moreSection.description": "The NieR and Drakengard series share a common timeline which you can explore it at the link bellow. Also we have gathered thousands of official artworks, videos, and notable web resources. <strong>Beware there can be spoilers.</strong>", "home.moreSection.description": "The NieR and Drakengard series share a common timeline which you can explore it at the link bellow. Also we have gathered thousands of official artworks, videos, and notable web resources. <strong>Beware there can be spoilers.</strong>",
"home.linksSection.title": "Links", "home.linksSection.title": "Links",
"home.linksSection.description": "Do you have a <strong>question</strong>? Would like to share something with our <strong>community</strong>? Are you interested in <strong>contributing</strong> to this project? Whatever it is, you should find what you are looking for at the following links.", "home.linksSection.description": "Do you have a <strong>question</strong>? Would like to share something with our <strong>community</strong>? Are you interested in <strong>contributing</strong> to this project? Whatever it is, you should find what you are looking for at the following links.",
"settings.title": "Settings",
"settings.theme.title": "Theme",
"settings.theme.description": "Switch between dark/light mode",
"settings.language.title": "Language",
"settings.language.description": "Select preferred language",
"settings.currency.title": "Currency",
"settings.currency.description": "Select preferred currency",
"header.topbar.search.tooltip": "Search on this website", "header.topbar.search.tooltip": "Search on this website",
"header.topbar.theme.tooltip": "Switch between dark/light mode", "header.topbar.theme.tooltip": "Switch between dark/light mode",
"header.topbar.theme.dark": "Dark", "header.topbar.theme.dark": "Dark",

View File

@ -1,16 +1,15 @@
import type { AstroCookies } from "astro"; import { cache } from "src/utils/cachedPayload";
import en from "./en.json"; import en from "./en.json";
import fr from "./fr.json"; import fr from "./fr.json";
import ja from "./ja.json" import ja from "./ja.json";
import acceptLanguage from 'accept-language'; import acceptLanguage from "accept-language";
import { z } from "zod";
type WordingKeys = keyof typeof en; type WordingKeys = keyof typeof en;
const translationFiles: Record<string, Record<WordingKeys, string>> = { const translationFiles: Record<string, Record<WordingKeys, string>> = {
en, en,
fr, fr,
ja ja,
}; };
export const getI18n = async (locale: string) => { export const getI18n = async (locale: string) => {
@ -118,6 +117,13 @@ export const getI18n = async (locale: string) => {
return `«${key}»`; return `«${key}»`;
}, },
getLocalizedUrl: (url: string): string => `/${locale}${url}`, 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,
}; };
}; };
@ -140,15 +146,11 @@ const limitMatchToBalanceCurlyBraces = (
return match.substring(0, index); return match.substring(0, index);
}; };
export const locales = ["en", "es", "fr", "ja", "pt", "zh"] as const; export type Locale = string;
acceptLanguage.languages([...locales]);
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en"; export const defaultLocale: Locale = "en";
export const getCurrentLocale = (pathname: string): Locale | undefined => { export const getCurrentLocale = (pathname: string): Locale | undefined => {
for (const locale of locales) { for (const locale of cache.locales) {
if (pathname.startsWith(`/${locale}`)) { if (pathname.startsWith(`/${locale}`)) {
return locale; return locale;
} }
@ -156,29 +158,14 @@ export const getCurrentLocale = (pathname: string): Locale | undefined => {
return undefined; return undefined;
}; };
export const getPreferredLocale = (request: Request): Locale | undefined => { export const getBestAcceptedLanguage = (
return acceptLanguage.get(request.headers.get("Accept-Language")) as Locale | null ?? undefined; request: Request
}; ): Locale | undefined => {
acceptLanguage.languages(cache.locales);
export const getCookiePreferredLocale = (
cookies: AstroCookies return (
): string | undefined => { (acceptLanguage.get(
const alPrefLanguages = cookies.get("al_pref_languages"); request.headers.get("Accept-Language")
) as Locale | null) ?? undefined
try { );
const json = alPrefLanguages?.json();
const result = z.array(z.string()).nonempty().safeParse(json);
if (result.success) {
for (const value of result.data) {
if (locales.includes(value as Locale)) {
return value;
}
}
}
} catch (e) {
console.error(e);
return undefined;
}
return undefined;
}; };