Something

This commit is contained in:
DrMint 2024-01-28 16:34:47 +01:00
parent 1f448f14f0
commit e57b82d227
21 changed files with 834 additions and 211 deletions

View File

@ -20,7 +20,15 @@ export default defineConfig({
},
}),
],
i18n: {
defaultLocale: "en",
locales: ["en", "es", "fr", "ja", "pt", "zh"],
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: false,
strategy: "pathname",
},
},
server: {
port: 12499,
host: true,

BIN
bun.lockb

Binary file not shown.

31
middleware/server.ts Normal file
View File

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

View File

@ -12,20 +12,22 @@
},
"dependencies": {
"@astrojs/check": "^0.4.1",
"@astrojs/node": "^8.0.0",
"astro": "^4.2.1",
"astro-icon": "next",
"ua-parser-js": "^1.0.37"
"@astrojs/node": "^8.1.0",
"astro": "^4.2.5",
"astro-icon": "^1.0.3",
"tippy.js": "^6.3.7",
"ua-parser-js": "^1.0.37",
"zod": "^3.22.4"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.1.70",
"@iconify-json/material-symbols": "^1.1.71",
"@types/ua-parser-js": "^0.7.39",
"astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17",
"bun-types": "^1.0.24",
"bun-types": "^1.0.25",
"postcss-preset-env": "^9.3.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"npm-check-updates": "^16.14.12"
"npm-check-updates": "^16.14.14"
}
}

1
public/css/sanitize.min.css vendored Normal file
View File

@ -0,0 +1 @@
*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){cursor:default;line-height:1.5;overflow-wrap:break-word;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%}:where(body){margin:0}:where(h1){font-size:2em;margin:0.67em 0}:where(dl,ol,ul):where(dl,ol,ul){margin:0}:where(hr){color:inherit;height:0}:where(nav):where(ol,ul){list-style-type:none;padding:0}:where(nav li)::before{content:"\200B";float:left}:where(pre){font-family:monospace, monospace;font-size:1em;overflow:auto}:where(abbr[title]){text-decoration:underline;text-decoration:underline dotted}:where(b,strong){font-weight:bolder}:where(code,kbd,samp){font-family:monospace, monospace;font-size:1em}:where(small){font-size:80%}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}:where(iframe){border-style:none}:where(svg:not([fill])){fill:currentColor}:where(table){border-collapse:collapse;border-color:inherit;text-indent:0}:where(button,input,select){margin:0}:where(button,[type="button" i],[type="reset" i],[type="submit" i]){-webkit-appearance:button}:where(fieldset){border:1px solid #a0a0a0}:where(progress){vertical-align:baseline}:where(textarea){margin:0;resize:vertical}:where([type="search" i]){-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:0.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:where(dialog){background-color:white;border:solid;color:black;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:fit-content}:where(dialog:not([open])){display:none}:where(details > summary:first-of-type){display:list-item}:where([aria-busy="true" i]){cursor:progress}:where([aria-controls]){cursor:pointer}:where([aria-disabled="true" i],[disabled]){cursor:not-allowed}:where([aria-hidden="false" i][hidden]){display:initial}:where([aria-hidden="false" i][hidden]:not(:focus)){clip:rect(0, 0, 0, 0);position:absolute}

90
public/css/tippy.css Normal file
View File

@ -0,0 +1,90 @@
.tippy-box[data-animation="fade"][data-state="hidden"] {
opacity: 0;
}
[data-tippy-root] {
max-width: calc(100vw - 10px);
}
.tippy-box {
position: relative;
background-color: var(--color-elevation-2);
color: var(--color-base-1000);
border-radius: 0.5rem;
border: 1px solid var(--color-base-650);
box-shadow: 0 20px 25px -5px var(--color-shadow-2),
0 0 10px -6px var(--color-shadow-2);
transition-property: transform, visibility, opacity;
box-sizing: border-box;
}
.tippy-box[data-placement^="top"] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^="top"] > .tippy-arrow:before {
bottom: -8px;
left: 0;
border-width: 8px 8px 0;
border-top-color: initial;
transform-origin: center top;
}
.tippy-box[data-placement^="bottom"] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^="bottom"] > .tippy-arrow:before {
top: -8px;
left: 0;
border-width: 0 8px 8px;
border-bottom-color: initial;
transform-origin: center bottom;
}
.tippy-box[data-placement^="left"] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^="left"] > .tippy-arrow:before {
border-width: 8px 0 8px 8px;
border-left-color: initial;
right: -8px;
transform-origin: center left;
}
.tippy-box[data-placement^="right"] > .tippy-arrow {
left: 0;
}
.tippy-box[data-placement^="right"] > .tippy-arrow:before {
left: -8px;
border-width: 8px 8px 8px 0;
border-right-color: initial;
transform-origin: center right;
}
.tippy-box[data-inertia][data-state="visible"] {
transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11);
}
.tippy-arrow {
width: 1em;
height: 1em;
color: var(--color-base-650);
}
.tippy-arrow:before {
content: "";
position: absolute;
border-color: transparent;
border-style: solid;
}
.tippy-content {
position: relative;
padding: 1rem 1.5rem;
z-index: 1;
}
.tippy-box[data-placement^="top"] {
transform-origin: bottom;
}
.tippy-box[data-placement^="bottom"] {
transform-origin: top;
}
.tippy-box[data-placement^="left"] {
transform-origin: right;
}
.tippy-box[data-placement^="right"] {
transform-origin: left;
}
.tippy-box[data-state="hidden"] {
transform: scale(0.8);
opacity: 0;
}

View File

@ -8,6 +8,8 @@ interface Props {
title?: string;
description?: string;
illustration?: string;
illustrationSize?: string;
illustrationPosition?: string;
}
const {
@ -15,6 +17,8 @@ const {
description,
illustration,
breadcrumb = [],
illustrationSize = "contain",
illustrationPosition = "center",
} = Astro.props;
---
@ -45,11 +49,17 @@ const {
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style define:vars={{ illustration: `url(${illustration})` }}>
<style
define:vars={{
illustration: `url(${illustration})`,
illustrationSize,
illustrationPosition,
}}
>
header {
display: flex;
flex-direction: column;
gap: 24px;
gap: 1.5em;
& > #header-content {
display: grid;
@ -58,12 +68,13 @@ const {
& > #header-left {
display: flex;
flex-direction: column;
gap: 32px;
gap: 2em;
place-items: flex-start;
& > h1 {
font-family: var(--font-serif);
font-size: 48px;
font-size: 3em;
overflow-wrap: anywhere;
}
& > p {
@ -73,9 +84,9 @@ const {
& > #image-container {
background-image: var(--illustration);
background-size: contain;
background-size: var(--illustrationSize);
background-repeat: no-repeat;
background-position: right center;
background-position: right var(--illustrationPosition);
mask-image: linear-gradient(
to left,
rgba(0, 0, 0, 1) 50%,
@ -90,7 +101,8 @@ const {
}
main {
padding-top: 96px;
padding-bottom: 128px;
padding-top: 6em;
padding-bottom: 8em;
flex-grow: 1;
}
</style>

View File

@ -9,46 +9,82 @@ const { withLinks } = Astro.props;
---
<footer>
<div id="nav">
<p class="font-serif">Accords Library</p>
<a href="/"><Icon name="accords" />Home</a>
<a href="/timeline"
><Icon name="material-symbols:calendar-month-outline" />Timeline</a
>
<a href="https://gallery.accords-library.com/posts"
><Icon name="material-symbols:perm-media-outline" />Gallery</a
>
<a href="/videos"><Icon name="material-symbols:movie-outline" />Videos</a>
<a href="/archives"
><Icon name="material-symbols:folder-zip-outline" />Web archives</a
>
</div>
<div id="license">
This websites content is made available under <a
href="https://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a
> unless otherwise noted.
<a
href="https://creativecommons.org/licenses/by-sa/4.0/"
id="common-creative"
aria-label="CC-BY-SA 4.0 License"
>
<Icon name="creative-commons-brands" />
<Icon name="creative-commons-by-brands" />
<Icon name="creative-commons-sa-brands" />
</a>
{
withLinks && (
<div id="nav">
<p class="font-serif">Accords Library</p>
<div>
<a href="/">
<Icon name="accords" />
<p>Home</p>
</a>
<a href="/timeline">
<Icon name="material-symbols:calendar-month-outline" />
<p>Timeline</p>
</a>
<a href="https://gallery.accords-library.com/posts">
<Icon name="material-symbols:perm-media-outline" />
<p>Gallery</p>
</a>
<a href="/videos">
<Icon name="material-symbols:movie-outline" />
<p>Videos</p>
</a>
<a href="/archives">
<Icon name="material-symbols:folder-zip-outline" />
<p>Web archives</p>
</a>
</div>
</div>
)
}
<div id="license" class:list={{ "with-links": withLinks }}>
<div id="license-section">
This websites content is made available under <a
href="https://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a
> unless otherwise noted.
<a
href="https://creativecommons.org/licenses/by-sa/4.0/"
id="common-creative"
aria-label="CC-BY-SA 4.0 License"
class="pressable-icon"
>
<Icon name="creative-commons-brands" />
<Icon name="creative-commons-by-brands" />
<Icon name="creative-commons-sa-brands" />
</a>
</div>
{
withLinks && (
<div id="socials">
<a href="/discord">
<a
href="/discord"
class="pressable-icon"
aria-label="Join the community"
>
<Icon name="discord-brands" />
</a>
<a href="https://twitter.com/AccordsLibrary">
<a
href="https://twitter.com/AccordsLibrary"
class="pressable-icon"
aria-label="Get the latest updates"
>
<Icon name="x-brands" />
</a>
<a href="https://github.com/Accords-Library">
<a
href="https://github.com/Accords-Library"
class="pressable-icon"
aria-label="Join the technical side"
>
<Icon name="github-brands" />
</a>
<a href="/contact">
<a
href="/contact"
aria-label="Send us an email"
class="pressable-icon"
>
<Icon name="material-symbols:mail-outline" />
</a>
</div>
@ -66,57 +102,171 @@ const { withLinks } = Astro.props;
<style>
footer {
border-top: 1px solid var(--color-base-1000);
padding-top: 32px;
border-top: 0.1em solid var(--color-base-1000);
padding-top: 2em;
display: flex;
place-content: center;
gap: clamp(24px, 12px + 2vw, 64px);
font-size: 14px;
gap: clamp(1.5em, 1.25em + 2vw, 4em);
font-size: 0.85em;
& > div {
max-width: 20em;
}
& > #nav {
display: flex;
flex-direction: row;
display: grid;
@media (max-width: 35rem) {
place-items: center;
}
& > p {
font-weight: 700;
font-size: 1.2em;
white-space: pre;
@media (max-width: 35rem) {
font-size: clamp(1em, 8vw, 2.5em);
}
}
& > div {
display: grid;
flex-direction: column;
place-items: start;
margin-top: 0.8em;
gap: 0.3em;
grid-template-columns: 1fr 1fr;
margin-left: -0.6em;
@media (max-width: 65rem) {
grid-template-columns: 1fr;
margin-top: 0.5em;
gap: unset;
}
@media (max-width: 35rem) {
gap: 0.25em 0.5em;
grid-template-columns: 1fr 1fr;
margin-top: 0.8em;
margin-left: unset;
font-size: 1.2em;
}
@media (max-width: 22rem) {
grid-template-columns: 1fr;
margin-top: 0.8em;
margin-left: unset;
font-size: 1.2em;
}
& > a {
display: flex;
place-items: center;
text-decoration: none;
gap: 0.4em;
padding: 0.4em 0.6em;
border-radius: 9999px;
@media (max-width: 35rem) {
padding: 0.6em 0.8em;
}
transition: 150ms background-color;
&:hover {
background-color: var(--color-base-250);
}
&:active {
background-color: var(--color-base-300);
}
& > svg {
flex-shrink: 0;
height: 0.75em;
width: 0.75em;
}
}
}
}
& > #license {
border-left: 1px solid var(--color-base-1000);
display: grid;
grid-template-areas: "license";
padding-left: 1em;
& > #common-creative {
display: flex;
justify-content: flex-start;
gap: 0.2em;
margin-top: 8px;
@media (max-width: 35rem) {
gap: 3em;
}
& > svg {
width: 16px;
height: 16px;
&.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;
}
}
& > #license-section {
grid-area: license;
& > #common-creative {
display: flex;
justify-content: flex-start;
gap: 0.2em;
margin-top: 0.5em;
& > svg {
width: 1em;
height: 1em;
}
@media (max-width: 35rem) {
place-content: center;
gap: clamp(4px, 2vw, 8px);
& > svg {
width: clamp(1em, 6vw, 24px);
height: clamp(1em, 6vw, 24px);
}
}
}
}
& > #socials {
grid-area: socials;
display: flex;
gap: 24px;
gap: 1.5em;
margin-top: 24px;
& > a > svg {
width: 24px;
height: 24px;
width: 1.5em;
height: 1.5em;
}
@media (max-width: 35rem) {
place-content: center;
gap: clamp(24px, 8vw, 48px);
& > a > svg {
width: clamp(24px, 8vw, 48px);
height: clamp(24px, 8vw, 48px);
}
}
}
}
& > #copyright {
border-left: 1px solid var(--color-base-1000);
border-left: 0.1em solid var(--color-base-1000);
padding-left: 1em;
@media (max-width: 35rem) {
border: none;
padding-left: unset;
}
}
@media (max-width: 35rem) {
@ -127,33 +277,6 @@ const { withLinks } = Astro.props;
& > div {
max-width: unset;
}
& > #license {
& > #common-creative {
place-content: center;
gap: clamp(4px, 2vw, 8px);
& > svg {
width: clamp(16px, 6vw, 24px);
height: clamp(16px, 6vw, 24px);
}
}
& > #socials {
place-content: center;
gap: clamp(24px, 8vw, 48px);
& > a > svg {
width: clamp(24px, 8vw, 48px);
height: clamp(24px, 8vw, 48px);
}
}
}
& > #copyright {
border: none;
padding-left: unset;
}
}
}
</style>

View File

@ -11,10 +11,20 @@ const userAgent = Astro.request.headers.get("user-agent") ?? "";
const parser = new UAParser(userAgent);
const isIOS = parser.getOS().name === "iOS";
const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
/* -------------------------------------------- HTML -------------------------------------------- */
---
<html lang="en">
<html
lang="en"
style={{ fontSize: "200%" }}
class:list={{
"manual-theme": prefTheme !== undefined,
"light-theme": prefTheme === "light",
"dark-theme": prefTheme === "dark",
}}
>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
@ -23,6 +33,7 @@ const isIOS = parser.getOS().name === "iOS";
<!-- Fonts google -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="/css/tippy.css" />
<link
href="https://fonts.googleapis.com/css2?family=Vollkorn:wght@400;500;600;700;800;900&family=Zen+Maru+Gothic:wght@400;500;700;900&display=swap"
rel="stylesheet"
@ -49,12 +60,8 @@ const isIOS = parser.getOS().name === "iOS";
}
<style is:global>
:root {
--font-serif: "Vollkorn", serif;
/* Get in between colors with https://colorkit.io/ */
@media (prefers-color-scheme: light) {
html {
&.light-theme {
--color-base-0: #ffffff;
--color-base-50: #fffaf3;
--color-base-100: #fff4e6;
@ -90,7 +97,7 @@ const isIOS = parser.getOS().name === "iOS";
--texture-dots-blend: multiply;
}
@media (prefers-color-scheme: dark) {
&.dark-theme {
--color-base-1000: #ebeae7;
--color-base-950: #eae5e0;
--color-base-900: #e8dfd8;
@ -125,6 +132,85 @@ const isIOS = parser.getOS().name === "iOS";
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
}
&: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;
}
@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;
}
}
}
:root {
--font-serif: "Vollkorn", serif;
}
/* THEMING */
@ -142,6 +228,10 @@ const isIOS = parser.getOS().name === "iOS";
padding-left: clamp(24px, 4vw, 64px);
padding-right: clamp(24px, 4vw, 64px);
padding-bottom: clamp(24px, 6vmin, 48px);
min-height: 100vb;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
h1,
@ -179,6 +269,18 @@ const isIOS = parser.getOS().name === "iOS";
background-repeat: repeat;
}
.pressable-icon {
transition: 150ms color;
&:hover {
color: var(--color-base-700);
}
&:active {
color: var(--color-base-550);
}
}
.keycap {
transition-duration: 150ms;
transition-property: translate, box-shadow, background-color;

View File

@ -2,12 +2,15 @@
import { Icon } from "astro-icon/components";
import Button from "components/Button.astro";
import ButtonGroup from "components/ButtonGroup.astro";
import Tooltip from "pages/_components/Tooltip.astro";
import astroConfig from "astro.config";
interface Props {
breadcrumb: { name: string; slug: string }[];
}
const { breadcrumb } = Astro.props;
const currentLocate = Astro.currentLocale ?? "en";
---
{
@ -20,12 +23,21 @@ const { breadcrumb } = Astro.props;
breadcrumb.length > 0 && (
<>
<a href="/">
<Icon name="accords" /> Home
<>
<Icon name="accords" width={16} height={16} />
<p>Home</p>
</>
</a>
{breadcrumb.map(({ name, slug }) => (
<>
<Icon name="material-symbols:arrow-forward-ios" />
<a href={slug}>{name}</a>
<Icon
name="material-symbols:arrow-forward-ios"
width={12}
height={12}
/>
<a href={slug}>
<p>{name}</p>
</a>
</>
))}
</>
@ -35,20 +47,29 @@ const { breadcrumb } = Astro.props;
<div id="toolbar">
<Button icon="material-symbols:search" ariaLabel="Search on this website" />
<div class="separator"></div>
<Button
class="xs-not"
icon="material-symbols:sunny-outline"
ariaLabel="Switch between dark/light mode"
/>
<Tooltip class="xs-not">
<Fragment slot="tooltip-content">
<a href="?action-theme=dark">Dark</a>
<a href="?action-theme=auto">Auto</a>
<a href="?action-theme=light">Light</a>
</Fragment>
<Button
icon="material-symbols:sunny-outline"
ariaLabel="Switch between dark/light mode"
/>
</Tooltip>
<ButtonGroup
buttons={[
{
icon: "material-symbols:text-decrease",
ariaLabel: "Decrease text size",
href: "?action-fontsize-increase=1",
},
{
icon: "material-symbols:text-increase",
ariaLabel: "Increase text size",
href: "?action-fontsize-decrease=1",
},
]}
/>
@ -70,12 +91,22 @@ const { breadcrumb } = Astro.props;
icon="material-symbols:currency-exchange"
ariaLabel="Select preferred currency"
/>
<Button
class="m-not"
icon="material-symbols:translate"
title="EN"
ariaLabel="Select preferred language"
/>
<Tooltip class="m-not">
<Fragment slot="tooltip-content">
{
astroConfig.i18n?.locales.map((locale) => (
<a href={`?action-lang=${locale}`}>
{locale.toString().toUpperCase()}
</a>
))
}
</Fragment>
<Button
icon="material-symbols:translate"
title={currentLocate.toUpperCase()}
ariaLabel="Select preferred language"
/>
</Tooltip>
<Button
class="m-not"
icon="material-symbols:currency-exchange"
@ -108,18 +139,20 @@ const { breadcrumb } = Astro.props;
& > a {
text-decoration: none;
flex-shrink: 0;
padding: 0.2em 0.5em;
padding-bottom: 0.3em;
border-radius: 12px;
display: flex;
place-items: center;
gap: 0.4em;
padding: 0.4em 0.6em;
border-radius: 9999px;
transition: 150ms background-color;
&:hover {
background-color: var(--color-elevation-1);
background-color: var(--color-base-250);
}
&:active {
background-color: var(--color-elevation-2);
background-color: var(--color-base-300);
}
&:last-child {
@ -153,39 +186,35 @@ const { breadcrumb } = Astro.props;
display: none;
}
}
}
}
</style>
<style is:global>
nav#topbar > #toolbar {
& > .m-only,
& > .s-only {
display: none;
}
@media (max-width: 40rem) {
& > .m-only {
display: flex;
}
& > .m-not {
& > :global(.m-only),
& > :global(.s-only) {
display: none;
}
@media (max-width: 28rem) {
& > .s-only {
@media (max-width: 40rem) {
& > :global(.m-only) {
display: flex;
}
& > .s-not {
& > :global(.m-not) {
display: none;
}
}
@media (max-width: 25rem) {
& > .xs-not {
display: none;
@media (max-width: 28rem) {
& > :global(.s-only) {
display: flex;
}
& > :global(.s-not) {
display: none;
}
}
@media (max-width: 25rem) {
& > :global(.xs-not) {
display: none;
}
}
}
}

View File

@ -2,13 +2,14 @@
import { Icon } from "astro-icon/components";
interface Props {
id?: string;
title?: string;
icon?: string | string[];
class?: string;
ariaLabel?: string;
}
const { title, icon, class: className, ariaLabel } = Astro.props;
const { title, icon, class: className, ariaLabel, id } = Astro.props;
const icons =
icon === undefined ? [] : typeof icon === "string" ? [icon] : icon;
@ -19,10 +20,11 @@ const icons =
}
<button
id={id}
class:list={[{ "with-title": !!title }, className]}
aria-label={ariaLabel}
>
{icons.map((cIcon) => <Icon name={cIcon} height={24} width={24} />)}
{icons.map((cIcon) => <Icon name={cIcon} />)}
{title}
</button>
@ -34,17 +36,17 @@ const icons =
button {
--foreground-color: var(--color-base-650);
color: var(--foreground-color);
border: 1px solid 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: 38px;
height: 2.5em;
display: flex;
place-items: center;
place-content: center;
gap: 10px;
gap: 1em;
font-weight: 700;
font-size: 1em;
@ -55,27 +57,28 @@ const icons =
border-color;
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
&.with-title > svg {
width: 1.2em;
height: 1.2em;
}
svg {
width: 1.5em;
height: 1.5em;
}
&:hover {
--foreground-color: var(--color-base-1000);
box-shadow: inset 0 1px 1px 0px var(--color-shadow-2);
box-shadow: inset 0 0.1em 0.1em 0 var(--color-shadow-2);
}
&:active {
transition-duration: 75ms;
--foreground-color: var(--color-base-1000);
background-color: var(--color-elevation-2);
box-shadow: inset 0 1px 1px 1px var(--color-shadow-2);
box-shadow: inset 0 0.1em 0.1em 0.1em var(--color-shadow-2);
& > div {
translate: 0 1px;
}
}
&.with-title {
& > div > svg {
width: 18px;
height: 18px;
}
translate: 0 0.1em;
}
}
</style>

View File

@ -1,8 +1,12 @@
---
import Button from "components/Button.astro";
type ButtonProps = Parameters<typeof Button>[0] & {
href?: string;
};
interface Props {
buttons: Parameters<typeof Button>[0][];
buttons: ButtonProps[];
}
const { buttons } = Astro.props;
@ -13,34 +17,49 @@ const { buttons } = Astro.props;
}
<div class="button-group">
{buttons.map((button) => <Button {...button} />)}
{
buttons.map(({ href, ...otherProps }) =>
href ? (
<a href={href}>
<Button {...otherProps} />
</a>
) : (
<Button {...otherProps} />
)
)
}
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style is:global>
<style>
.button-group {
display: flex;
gap: 0;
& > button:first-child {
border-top-left-radius: 9999px;
border-bottom-left-radius: 9999px;
border-right: unset;
padding-right: 0.5em;
}
& > :global(*) {
&:is(button),
& > :global(button) {
border-radius: 0;
}
& > button {
border-radius: 0;
}
&:first-child:is(button),
&:first-child > :global(button) {
border-top-left-radius: 9999px;
border-bottom-left-radius: 9999px;
border-right: unset;
padding-right: 0.5em;
}
& > button:last-child {
border-top-right-radius: 9999px;
border-bottom-right-radius: 9999px;
border-left: unset;
padding-left: 0.5em;
&:last-child:is(button),
&:last-child > :global(button) {
border-top-right-radius: 9999px;
border-bottom-right-radius: 9999px;
border-left: unset;
padding-left: 0.5em;
}
}
}
</style>

124
src/middleware.ts Normal file
View File

@ -0,0 +1,124 @@
import type { AstroCookies } from "astro";
import { defineMiddleware, sequence } from "astro:middleware";
import { z } from "zod";
import astroConfig from "astro.config";
const cookieThemeSchema = z.enum(["dark", "light", "auto"]);
const getAbsoluteLocaleUrl = (locale: string, url: string) =>
`/${locale}${url}`;
const redirection = (
redirectURL: string,
headers: Record<string, string> = {}
): Response => {
return new Response(undefined, {
headers: { ...headers, Location: redirectURL },
status: 302,
statusText: "Found",
});
};
export const langMiddleware = defineMiddleware(
({ cookies, preferredLocale, currentLocale, url }, next) => {
const cookiePreferredLocale = getCookiePreferredLocale(cookies);
const actionLang = url.searchParams.get("action-lang");
if (!currentLocale) {
currentLocale = cookiePreferredLocale ?? preferredLocale ?? "en";
const redirectURL = getAbsoluteLocaleUrl(currentLocale, url.pathname);
return redirection(redirectURL);
}
if (actionLang && actionLang !== cookiePreferredLocale) {
const pathnameWithoutLocale = url.pathname.substring(
currentLocale.length + 1
);
const redirectURL = getAbsoluteLocaleUrl(
actionLang,
pathnameWithoutLocale
);
return redirection(redirectURL, {
"Set-Cookie": `al_pref_languages=${JSON.stringify([
actionLang,
])}; Path=/`,
});
}
if (cookiePreferredLocale) {
if (cookiePreferredLocale !== currentLocale) {
const pathnameWithoutLocale = url.pathname.substring(
currentLocale.length + 1
);
const redirectURL = getAbsoluteLocaleUrl(
cookiePreferredLocale,
pathnameWithoutLocale
);
return redirection(redirectURL);
}
} else if (preferredLocale) {
if (preferredLocale !== currentLocale) {
const pathnameWithoutLocale = url.pathname.substring(
currentLocale.length + 1
);
const redirectURL = getAbsoluteLocaleUrl(
preferredLocale,
pathnameWithoutLocale
);
return redirection(redirectURL);
}
}
return next();
}
);
export const headersMiddleware = defineMiddleware(
async ({ currentLocale, url }, next) => {
const actionTheme = url.searchParams.get("action-theme");
const verifiedActionTheme = cookieThemeSchema.safeParse(actionTheme);
if (verifiedActionTheme.success) {
url.searchParams.delete("action-theme");
if (verifiedActionTheme.data === "auto") {
return redirection(url.toString(), {
"Set-Cookie": `al_pref_theme=; Path=/; Expires=${new Date(0).toUTCString()}`,
});
}
return redirection(url.toString(), {
"Set-Cookie": `al_pref_theme=${verifiedActionTheme.data}; Path=/`,
});
}
const response = await next();
if (currentLocale) {
response.headers.set("Content-Language", currentLocale);
}
return response;
}
);
export const onRequest = sequence(headersMiddleware, langMiddleware);
const getCookiePreferredLocale = (
cookies: AstroCookies
): string | undefined => {
const alPrefLanguages = cookies.get("al_pref_languages");
try {
const json = alPrefLanguages?.json();
const result = z.array(z.string()).nonempty().safeParse(json);
if (result.success) {
for (const value of result.data) {
if (astroConfig.i18n?.locales.includes(value)) {
return value;
}
}
}
} catch (e) {
console.error(e);
return undefined;
}
return undefined;
};

1
src/pages/404.astro Normal file
View File

@ -0,0 +1 @@
<h1>Oh nyo...</h1>

View File

@ -20,7 +20,7 @@ const { icon, title, href } = Astro.props;
a {
display: flex;
place-items: center;
gap: 16px;
gap: 1em;
color: var(--color-base-1000);
padding: 24px;
padding-top: 12px;

View File

@ -10,8 +10,6 @@ import FolderCard from "./_components/FolderCard.astro";
<AppLayout
breadcrumb={[
{ name: "Drakengard", slug: "drakengard" },
{ name: "Testing a long name", slug: "long" },
{ name: "More stuff", slug: "more" },
]}
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."

View File

@ -2,15 +2,20 @@
import { Icon } from "astro-icon/components";
import AppLayout from "components/AppLayout/AppLayout.astro";
import Button from "components/Button.astro";
import LinkCard from "./_components/LinkCard.astro";
import CategoryCard from "./_components/CategoryCard.astro";
import LinkCard from "../_components/LinkCard.astro";
import CategoryCard from "../_components/CategoryCard.astro";
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<AppLayout title="Accords Library" illustration="/img/bg-home.webp">
<AppLayout
title="Accords Library"
illustration="/img/bg-home.webp"
illustrationSize="100vh"
illustrationPosition="20%"
>
<div id="title" slot="header-title">
<Icon name="accords" />
<div>
@ -23,13 +28,12 @@ import CategoryCard from "./_components/CategoryCard.astro";
We aim at archiving and translating all of <strong>Yoko Taro</strong>s
works.<br />Yoko Taro is a Japanese video game director and scenario
writer. He is best-known for his involvement with the <strong>NieR</strong
> and <strong>Drakengard</strong> series. To complement his games, Yoko
Taro likes to publish side materials in the form of books, anime, manga,
audio books, novellas, even theater plays.<br />These media can be very
difficult to find. His work goes all the way back to 2003. Most of it was
released solely in Japanese, and sometimes in short supply. So this is
what we do here: <strong>discover, archive, translate, and analyze</strong
>.
> and <strong>Drakengard</strong> series. To complement his games, Yoko Taro
likes to publish side materials in the form of books, anime, manga, audio books,
novellas, even theater plays.<br />These media can be very difficult to
find. His work goes all the way back to 2003. Most of it was released
solely in Japanese, and sometimes in short supply. So this is what we do
here: <strong>discover, archive, translate, and analyze</strong>.
</p>
<Button title="Read more about us" icon="material-symbols:left-click" />
</div>
@ -195,8 +199,8 @@ import CategoryCard from "./_components/CategoryCard.astro";
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.
> to this project? Whatever it is, you should find what you are looking
for at the following links.
</p>
<div class="grid">
<LinkCard
@ -239,6 +243,7 @@ import CategoryCard from "./_components/CategoryCard.astro";
flex-direction: column;
gap: 24px;
align-items: flex-start;
margin-bottom: 128px;
& > p {
line-height: 1.4;
@ -257,7 +262,7 @@ import CategoryCard from "./_components/CategoryCard.astro";
#title {
display: flex;
place-items: center;
gap: 16px;
gap: 1em;
& > svg {
width: 64px;
@ -314,6 +319,7 @@ import CategoryCard from "./_components/CategoryCard.astro";
#main {
display: grid;
gap: 64px;
margin-top: -96px;
& > section {
& > h2 {
@ -342,9 +348,9 @@ import CategoryCard from "./_components/CategoryCard.astro";
& > .grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
column-gap: clamp(6px, 2vmin, 16px);
row-gap: clamp(6px + 6px, 2vmin + 6px, 16px);
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
column-gap: clamp(6px, 2vmin, 1em);
row-gap: clamp(6px + 6px, 2vmin + 6px, 1em);
}
}
}

View File

@ -18,6 +18,26 @@ const { img, name, href } = Astro.props;
"dark-image": `url(${img?.dark})`,
}}
>
:global(html):not(.manual-theme) {
a > div {
@media (prefers-color-scheme: light) {
background-image: var(--light-image);
}
@media (prefers-color-scheme: dark) {
background-image: var(--dark-image);
}
}
}
:global(html).light-theme a > div {
background-image: var(--light-image);
}
:global(html).dark-theme a > div {
background-image: var(--dark-image);
}
a {
font-size: 24px;
font-weight: 400;
@ -32,14 +52,6 @@ const { img, name, href } = Astro.props;
user-select: none;
& > div {
@media (prefers-color-scheme: light) {
background-image: var(--light-image);
}
@media (prefers-color-scheme: dark) {
background-image: var(--dark-image);
}
background-size: contain;
background-position: center;
background-repeat: no-repeat;

View File

@ -22,16 +22,16 @@ const { icon, subtitle, title, href } = Astro.props;
a {
display: flex;
place-items: center;
gap: 16px;
gap: 1em;
color: var(--color-base-1000);
padding: 24px;
padding-top: 12px;
border-radius: 12px;
padding: 1.5em;
padding-top: 0.75em;
border-radius: 0.75em;
text-decoration: none;
& > svg {
width: clamp(24px, 6vw + 14px, 48px);
height: clamp(24px, 6vw + 14px, 48px);
width: clamp(1.5em, 6vw + 0.8em, 3em);
height: clamp(1.5em, 6vw + 0.8em, 3em);
}
& > #right {
@ -41,7 +41,7 @@ const { icon, subtitle, title, href } = Astro.props;
gap: 2px;
& > h3 {
font-size: 24px;
font-size: 2em;
}
& > p {

View File

@ -0,0 +1,32 @@
---
interface Props {
class?: string;
}
const { class: className } = Astro.props;
---
<tippy-tooltip class={className}>
<template><slot name="tooltip-content" /></template>
<slot />
</tippy-tooltip>
<script>
import tippy from "tippy.js";
class TippyTooltip extends HTMLElement {
constructor() {
super();
tippy(this, {
allowHTML: true,
content: (ref) =>
ref.querySelector(":scope > template")?.innerHTML ?? "",
interactive: true,
trigger: "click",
});
}
}
customElements.define("tippy-tooltip", TippyTooltip);
</script>

30
tests/i18n.test.ts Normal file
View File

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