Bring over improvements from badmanners.xyz
This commit is contained in:
parent
d022fab5d6
commit
c55c82633d
24 changed files with 542 additions and 444 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
src/components/AutoDarkMode.astro
|
src/components/AgeRestrictedScriptInline.astro
|
||||||
|
src/components/DarkModeScriptInline.astro
|
||||||
.astro/
|
.astro/
|
||||||
dist/
|
dist/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { join } from "node:path";
|
import { join, normalize as n } from "node:path/posix";
|
||||||
import { program, Option } from "commander";
|
import { program, Option } from "commander";
|
||||||
|
|
||||||
interface DeployLftpOptions {
|
interface DeployLftpOptions {
|
||||||
|
|
@ -18,8 +18,8 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
|
||||||
"-c",
|
"-c",
|
||||||
[
|
[
|
||||||
`open -u ${user},${password} ${host}`,
|
`open -u ${user},${password} ${host}`,
|
||||||
`mirror --reverse --include-glob ${join(assetsFolder, "*")} --delete --only-missing --no-perms --verbose ${sourceFolder} ${targetFolder}`,
|
`mirror --reverse --include-glob ${join(assetsFolder, "*")} --delete --only-missing --no-perms --verbose ${n(sourceFolder)} ${n(targetFolder)}`,
|
||||||
`mirror --reverse --exclude-glob ${join(assetsFolder, "*")} --delete --no-perms --verbose ${sourceFolder} ${targetFolder}`,
|
`mirror --reverse --exclude-glob ${join(assetsFolder, "*")} --delete --no-perms --verbose ${n(sourceFolder)} ${n(targetFolder)}`,
|
||||||
`bye`,
|
`bye`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
],
|
],
|
||||||
|
|
@ -28,13 +28,9 @@ async function deployLftp({ host, user, password, targetFolder, sourceFolder, as
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
process.on("close", (code) => {
|
process.on("close", (code) =>
|
||||||
if (code === 0) {
|
(code === 0) ? resolve() : reject(`lftp failed with code ${code}`),
|
||||||
resolve();
|
);
|
||||||
} else {
|
|
||||||
reject(`deploy-lftp failed with code ${code}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { type ChildProcess, exec, spawnSync } from "node:child_process";
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
import { readdir, mkdir, mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
|
import { readdir, mkdir, mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join as pathJoin, normalize } from "node:path";
|
import { join, normalize } from "node:path";
|
||||||
import { setTimeout } from "node:timers/promises";
|
import { createInterface } from "node:readline";
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
import fetchRetryWrapper from "fetch-retry";
|
import fetchRetryWrapper from "fetch-retry";
|
||||||
|
|
||||||
|
|
@ -31,26 +31,16 @@ function getRTFStyles(rtfSource: string) {
|
||||||
|
|
||||||
const fetchRetry = fetchRetryWrapper(global.fetch);
|
const fetchRetry = fetchRetryWrapper(global.fetch);
|
||||||
|
|
||||||
interface AstroApiResponse {
|
|
||||||
story: string;
|
|
||||||
description: Record<string, string>;
|
|
||||||
thumbnail: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLibreOfficeRunning = async () =>
|
const isLibreOfficeRunning = async () =>
|
||||||
new Promise<boolean>((res, rej) => {
|
new Promise<boolean>((res) => {
|
||||||
exec("ps -ax", (err, stdout) => {
|
spawn("ps", ["-ax"], {stdio: 'pipe'});
|
||||||
if (err) {
|
const lines = createInterface({ input: spawn("ps", ["-ax"], {stdio: 'pipe'}).stdout });
|
||||||
rej(err);
|
lines.on("line", (line) => {
|
||||||
return;
|
if (line.includes("libreoffice") && line.includes("--writer")) {
|
||||||
|
res(true);
|
||||||
}
|
}
|
||||||
res(
|
|
||||||
stdout
|
|
||||||
.toLowerCase()
|
|
||||||
.split("\n")
|
|
||||||
.some((line) => line.includes("libreoffice") && line.includes("--writer")),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
lines.on("close", () => res(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function exportStory(slug: string, options: { outputDir: string }) {
|
async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
|
|
@ -72,53 +62,53 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
console.error(`ERROR: Directory ${outputDir} is not empty!`);
|
console.error(`ERROR: Directory ${outputDir} is not empty!`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
/* Check if Astro development server needs to be spawned */
|
|
||||||
const healthcheckURL = `http://localhost:4321/api/healthcheck`;
|
|
||||||
let devServerProcess: ChildProcess | null = null;
|
|
||||||
try {
|
|
||||||
const response = await fetchRetry(healthcheckURL, { retries: 3, retryDelay: 1000 });
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
const healthcheck = await response.json();
|
|
||||||
if (!healthcheck.isAlive) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* Spawn Astro dev server */
|
/* Spawn Astro dev server */
|
||||||
console.log("Starting Astro development server...");
|
console.log("Starting Astro development server...");
|
||||||
devServerProcess = exec("./node_modules/.bin/astro dev");
|
const devServerProcess = spawn("./node_modules/.bin/astro", ["dev"], { stdio: 'pipe' });
|
||||||
await setTimeout(2000);
|
const promise = new Promise<string>((resolve, reject) => {
|
||||||
try {
|
const localServerRegex = /Local\s+(http\S+)/
|
||||||
const response = await fetchRetry(healthcheckURL, { retries: 5, retryDelay: 2000 });
|
const lines = createInterface({ input: devServerProcess.stdout });
|
||||||
if (!response.ok) {
|
lines.on("line", (line) => {
|
||||||
throw new Error();
|
const match = localServerRegex.exec(line);
|
||||||
|
if (match && match[1]) {
|
||||||
|
resolve(match[1]);
|
||||||
}
|
}
|
||||||
const healthcheck = await response.json();
|
});
|
||||||
if (!healthcheck.isAlive) {
|
lines.on("close", reject);
|
||||||
throw new Error();
|
});
|
||||||
|
const astroURL = await promise;
|
||||||
|
console.log(`Astro listening on ${astroURL}`);
|
||||||
|
try {
|
||||||
|
const response = await fetchRetry(new URL(`/api/healthcheck`, astroURL), { retries: 5, retryDelay: 2000 });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
const healthcheck: { isAlive: boolean } = await response.json();
|
||||||
|
if (healthcheck.isAlive !== true) {
|
||||||
|
throw new Error(JSON.stringify(healthcheck));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.error("ERROR: Astro dev server didn't respond in time!");
|
console.error("ERROR: Astro dev server didn't respond in time!");
|
||||||
devServerProcess && devServerProcess.kill();
|
if (!devServerProcess.kill("SIGTERM")) {
|
||||||
devServerProcess = null;
|
console.error("WARNING: Unable to shut down Astro dev server!");
|
||||||
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/* Get data (story, thumbnail, descriptions) from Astro development server */
|
/* Get data (story, thumbnail, descriptions) from Astro development server */
|
||||||
let storyText = "";
|
let storyText = "";
|
||||||
try {
|
try {
|
||||||
console.log("Getting data from Astro...");
|
console.log("Getting data from Astro...");
|
||||||
|
const response = await fetch(new URL(`/api/export-story/${slug}`, astroURL));
|
||||||
const response = await fetch(`http://localhost:4321/api/export-story/${slug}`);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to reach API (status code ${response.status})`);
|
throw new Error(`Failed to reach API (status code ${response.status})`);
|
||||||
}
|
}
|
||||||
const data: AstroApiResponse = await response.json();
|
const data: { story: string, description: Record<string, string>, thumbnail: string | null } = await response.json();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(data.description).map(async ([website, description]) => {
|
Object.entries(data.description).map(async ([website, description]) => {
|
||||||
return await writeFile(
|
return await writeFile(
|
||||||
pathJoin(outputDir, `description_${website}.${website === "weasyl" ? "md" : "txt"}`),
|
join(outputDir, `description_${website}.${website === "weasyl" ? "md" : "txt"}`),
|
||||||
description,
|
description,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
@ -128,42 +118,41 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
const thumbnailPath = data.thumbnail
|
const thumbnailPath = data.thumbnail
|
||||||
.replace(/^\/@fs/, "")
|
.replace(/^\/@fs/, "")
|
||||||
.replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, "");
|
.replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, "");
|
||||||
await copyFile(thumbnailPath, pathJoin(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
|
await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
|
||||||
} else {
|
} else {
|
||||||
const thumbnail = await fetch(data.thumbnail);
|
const thumbnail = await fetch(data.thumbnail);
|
||||||
if (!thumbnail.ok) {
|
if (!thumbnail.ok) {
|
||||||
throw new Error("Failed to get thumbnail");
|
throw new Error("Failed to get thumbnail");
|
||||||
}
|
}
|
||||||
const thumbnailExt = thumbnail.headers.get("Content-Type")?.startsWith("image/png") ? "png" : "jpg";
|
const thumbnailExt = thumbnail.headers.get("Content-Type")?.startsWith("image/png") ? "png" : "jpg";
|
||||||
await writeFile(pathJoin(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storyText = data.story;
|
storyText = data.story;
|
||||||
writeFile(pathJoin(outputDir, `${slug}.txt`), storyText);
|
writeFile(join(outputDir, `${slug}.txt`), storyText);
|
||||||
} finally {
|
} finally {
|
||||||
if (devServerProcess) {
|
if (devServerProcess) {
|
||||||
console.log("Shutting down the Astro development server...");
|
console.log("Shutting down the Astro development server...");
|
||||||
if (!devServerProcess.kill("SIGTERM")) {
|
if (!devServerProcess.kill("SIGTERM")) {
|
||||||
console.error("WARNING: Unable to shut down Astro dev server!");
|
console.error("WARNING: Unable to shut down Astro dev server!");
|
||||||
}
|
}
|
||||||
devServerProcess = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Parse story into output formats */
|
/* Parse story into output formats */
|
||||||
console.log("Parsing story into output formats...");
|
console.log("Parsing story into output formats...");
|
||||||
await writeFile(pathJoin(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
await writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
||||||
const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
|
const tempDir = await mkdtemp(join(tmpdir(), "export-story-"));
|
||||||
await writeFile(pathJoin(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
||||||
spawnSync(
|
spawnSync(
|
||||||
"libreoffice",
|
"libreoffice",
|
||||||
["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, pathJoin(tempDir, "temp.txt")],
|
["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")],
|
||||||
{ stdio: "ignore" },
|
{ stdio: "ignore" },
|
||||||
);
|
);
|
||||||
const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
|
const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
|
||||||
const rtfStyles = getRTFStyles(rtfText);
|
const rtfStyles = getRTFStyles(rtfText);
|
||||||
await writeFile(
|
await writeFile(
|
||||||
pathJoin(outputDir, `${slug}.rtf`),
|
join(outputDir, `${slug}.rtf`),
|
||||||
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
||||||
);
|
);
|
||||||
console.log("Success!");
|
console.log("Success!");
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
---
|
---
|
||||||
|
import AgeRestrictedScriptInline from "./AgeRestrictedScriptInline.astro";
|
||||||
|
import IconTriangleExclamation from "./icons/IconTriangleExclamation.astro";
|
||||||
|
---
|
||||||
|
|
||||||
---
|
<div
|
||||||
|
style={{display: "none"}}
|
||||||
<template id="template-modal-age-restricted">
|
|
||||||
<div
|
|
||||||
id="modal-age-restricted"
|
id="modal-age-restricted"
|
||||||
class="fixed inset-0 bg-stone-100 dark:bg-stone-900"
|
class="fixed inset-0 bg-stone-100 dark:bg-stone-900"
|
||||||
tabindex="-1"
|
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-hidden="false"
|
aria-labelledby="title-age-restricted"
|
||||||
>
|
>
|
||||||
<div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
|
<div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
|
||||||
<div class="text-bm-500 dark:text-bm-400">
|
<div class="text-bm-500 dark:text-bm-400">
|
||||||
<svg style={{ width: "3rem", height: "3rem", fill: "currentColor" }} viewBox="0 0 512 512">
|
<IconTriangleExclamation width="3rem" height="3rem" />
|
||||||
<path
|
|
||||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-3 pt-2 text-3xl font-light text-stone-700 sm:pb-4 sm:pt-2 dark:text-stone-50">
|
<div id="title-age-restricted" class="pb-3 pt-2 text-3xl font-light text-stone-700 sm:pb-4 sm:pt-2 dark:text-stone-50">
|
||||||
Age verification
|
Age verification
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -47,40 +43,45 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<AgeRestrictedScriptInline />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
const ENABLE_VIEW_TRANSITIONS = false;
|
||||||
if (localStorage.getItem("ageVerified") !== "true") {
|
|
||||||
const fragment = document
|
const ageRestrictedModalSetup = () => {
|
||||||
.querySelector<HTMLElementTagNameMap["template"]>("template#template-modal-age-restricted")!
|
const modal = document.querySelector<HTMLElementTagNameMap["div"]>("body > div#modal-age-restricted");
|
||||||
.content.cloneNode(true) as DocumentFragment;
|
if (!modal) {
|
||||||
const modal = fragment.firstElementChild as HTMLElementTagNameMap["div"];
|
throw new Error("Missing #modal-age-restricted element! Make sure that it's a direct child of body.");
|
||||||
const controller = new AbortController();
|
}
|
||||||
const { signal } = controller;
|
let ageVerified: "true" | undefined = localStorage.ageVerified;
|
||||||
modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!.addEventListener(
|
if (ageVerified !== "true") {
|
||||||
"click",
|
const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
|
||||||
(e: MouseEvent) => {
|
const onRejectButtonClick = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
location.href = "about:blank";
|
location.href = "about:blank";
|
||||||
},
|
};
|
||||||
{ signal },
|
rejectButton.addEventListener("click", onRejectButtonClick);
|
||||||
);
|
|
||||||
modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!.addEventListener(
|
modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
controller.abort();
|
rejectButton.removeEventListener("click", onRejectButtonClick);
|
||||||
localStorage.setItem("ageVerified", "true");
|
ageVerified = "true";
|
||||||
|
localStorage.ageVerified = ageVerified;
|
||||||
document.body.style.overflow = "auto";
|
document.body.style.overflow = "auto";
|
||||||
modal.remove();
|
document.querySelectorAll("body > :not(#modal-age-restricted)").forEach((el) => el.removeAttribute("inert"));
|
||||||
|
modal.style.display = "none";
|
||||||
},
|
},
|
||||||
{ signal },
|
{ once: true },
|
||||||
);
|
);
|
||||||
document.body.style.overflow = "hidden";
|
rejectButton.focus();
|
||||||
document.body.appendChild(fragment);
|
}
|
||||||
document.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")?.focus();
|
};
|
||||||
|
if (ENABLE_VIEW_TRANSITIONS) {
|
||||||
|
document.addEventListener("astro:page-load", ageRestrictedModalSetup);
|
||||||
|
} else {
|
||||||
|
ageRestrictedModalSetup();
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
4
src/components/AgeRestrictedScriptInline.astro
Normal file
4
src/components/AgeRestrictedScriptInline.astro
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline>function a(){let b=document,c="#modal-age-restricted",d="true";localStorage.ageVerified!==d&&((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(e=>e.setAttribute("inert",d)),(b.querySelector("body > "+c).style.display="block"));}document.addEventListener("astro:after-swap",a);a()</script>
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
<script is:inline>!function(){var a="dark",b="auto",c="colorScheme",d=document.body.classList,e=localStorage,f=e.getItem(c);f&&f!==b?f===a&&d.add(a):(e.setItem(c,b),matchMedia("(prefers-color-scheme: dark)").matches&&d.add(a))}();</script>
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
---
|
---
|
||||||
import DarkModeInline from "./AutoDarkMode.astro";
|
import DarkModeScriptInline from "./DarkModeScriptInline.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<DarkModeInline />
|
<DarkModeScriptInline />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
const ENABLE_VIEW_TRANSITIONS = false;
|
||||||
let colorScheme = localStorage.getItem("colorScheme");
|
type ColorScheme = "auto" | "dark" | "light";
|
||||||
if (colorScheme == null || colorScheme === "auto") {
|
|
||||||
|
const colorSchemeSetup = () => {
|
||||||
|
let colorScheme: ColorScheme | undefined = localStorage.colorScheme;
|
||||||
|
if (!colorScheme || colorScheme === "auto") {
|
||||||
colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
}
|
}
|
||||||
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
|
const toggleColorScheme = (e: MouseEvent) => {
|
||||||
button.classList.remove("hidden");
|
|
||||||
button.style.removeProperty("display");
|
|
||||||
button.setAttribute("aria-hidden", "false");
|
|
||||||
button.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (colorScheme === "dark") {
|
if (colorScheme === "dark") {
|
||||||
colorScheme = "light";
|
colorScheme = "light";
|
||||||
|
|
@ -23,8 +22,18 @@ import DarkModeInline from "./AutoDarkMode.astro";
|
||||||
colorScheme = "dark";
|
colorScheme = "dark";
|
||||||
document.body.classList.add("dark");
|
document.body.classList.add("dark");
|
||||||
}
|
}
|
||||||
localStorage.setItem("colorScheme", colorScheme);
|
localStorage.colorScheme = colorScheme;
|
||||||
|
};
|
||||||
|
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
|
||||||
|
button.classList.remove("hidden");
|
||||||
|
button.style.removeProperty("display");
|
||||||
|
button.setAttribute("aria-hidden", "false");
|
||||||
|
button.addEventListener("click", toggleColorScheme);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
})();
|
if (ENABLE_VIEW_TRANSITIONS) {
|
||||||
|
document.addEventListener("astro:page-load", colorSchemeSetup);
|
||||||
|
} else {
|
||||||
|
colorSchemeSetup();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
4
src/components/DarkModeScriptInline.astro
Normal file
4
src/components/DarkModeScriptInline.astro
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline>function a(){var b="dark",c="auto",d="colorScheme",e=document.body.classList,f=localStorage,g=f&&f[d];g&&g!==c?g===b&&e.add(b):(f&&(f[d]=c),matchMedia("(prefers-color-scheme: dark)").matches&&e.add(b))};document.addEventListener("astro:after-swap",a);a()</script>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { Lang } from "../i18n";
|
import type { Lang } from "../i18n";
|
||||||
|
import IconFavorites from "./icons/IconFavorites.astro";
|
||||||
|
import IconReblogs from "./icons/IconReblogs.astro";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
lang: Lang;
|
lang: Lang;
|
||||||
|
|
@ -108,19 +110,11 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
<div class="ml-1 flex flex-row pb-2 pt-1">
|
<div class="ml-1 flex flex-row pb-2 pt-1">
|
||||||
<div class="flex" aria-label="Favorites">
|
<div class="flex" aria-label="Favorites">
|
||||||
<span data-favorites></span>
|
<span data-favorites></span>
|
||||||
<svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 576 512" aria-hidden="true">
|
<IconFavorites width="1.25rem" height="1.25rem" class="ml-2" />
|
||||||
<path
|
|
||||||
d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4 flex" aria-label="Reblogs">
|
<div class="ml-4 flex" aria-label="Reblogs">
|
||||||
<span data-reblogs></span>
|
<span data-reblogs></span>
|
||||||
<svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 512 512" aria-hidden="true">
|
<IconReblogs width="1.25rem" height="1.25rem" class="ml-2" />
|
||||||
<path
|
|
||||||
d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-comment-thread class="-mb-2" aria-hidden="true" aria-label="Replies"></div>
|
<div data-comment-thread class="-mb-2" aria-hidden="true" aria-label="Replies"></div>
|
||||||
|
|
@ -128,6 +122,8 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const ENABLE_VIEW_TRANSITIONS = false;
|
||||||
|
|
||||||
interface MastodonPost {
|
interface MastodonPost {
|
||||||
link: string;
|
link: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
|
|
@ -162,22 +158,20 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
emojis: CustomEmoji[];
|
emojis: CustomEmoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Context {
|
interface StatusContext {
|
||||||
ancestors: Status[];
|
ancestors: Status[];
|
||||||
descendants: Status[];
|
descendants: Status[];
|
||||||
}
|
}
|
||||||
|
|
||||||
(function () {
|
|
||||||
async function renderComments(section: Element, post: MastodonPost) {
|
async function renderComments(section: Element, post: MastodonPost) {
|
||||||
const commentsDescription = section.querySelector<HTMLElementTagNameMap["p"]>("p#comments-description")!;
|
const commentsDescription = section.querySelector<HTMLElementTagNameMap["p"]>("p#comments-description")!;
|
||||||
const loadCommentsButton = section.querySelector<HTMLElementTagNameMap["button"]>("button#load-comments-button")!;
|
const loadCommentsButton = section.querySelector<HTMLElementTagNameMap["button"]>("button#load-comments-button")!;
|
||||||
const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
|
const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
||||||
}
|
}
|
||||||
const data: Context = await response.json();
|
const data: StatusContext = await response.json();
|
||||||
|
|
||||||
const emojiTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
|
const emojiTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
|
||||||
"template#template-comment-emoji",
|
"template#template-comment-emoji",
|
||||||
|
|
@ -273,10 +267,11 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
if (commentsList.length) {
|
if (commentsList.length) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
commentsList.forEach((comment) => fragment.appendChild(comment));
|
commentsList.forEach((comment) => fragment.appendChild(comment));
|
||||||
commentsDiv.appendChild(fragment);
|
|
||||||
commentsDescription
|
commentsDescription
|
||||||
.querySelector<HTMLElementTagNameMap["span"]>("span[data-comments]")!
|
.querySelector<HTMLElementTagNameMap["span"]>("span[data-comments]")!
|
||||||
.style.removeProperty("display");
|
.style.removeProperty("display");
|
||||||
|
const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
||||||
|
commentsDiv.appendChild(fragment);
|
||||||
commentsDiv.style.removeProperty("display");
|
commentsDiv.style.removeProperty("display");
|
||||||
} else {
|
} else {
|
||||||
commentsDescription
|
commentsDescription
|
||||||
|
|
@ -326,6 +321,9 @@ const { link, instance, user, postId } = Astro.props;
|
||||||
loadCommentsButton.style.removeProperty("display");
|
loadCommentsButton.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ENABLE_VIEW_TRANSITIONS) {
|
||||||
|
document.addEventListener("astro:page-load", initCommentSection);
|
||||||
|
} else {
|
||||||
initCommentSection();
|
initCommentSection();
|
||||||
})();
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
15
src/components/icons/IconArrowBack.astro
Normal file
15
src/components/icons/IconArrowBack.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
|
||||||
|
<path
|
||||||
|
d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconArrowUp.astro
Normal file
15
src/components/icons/IconArrowUp.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 384 512">
|
||||||
|
<path
|
||||||
|
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
13
src/components/icons/IconChevronLeft.astro
Normal file
13
src/components/icons/IconChevronLeft.astro
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 320 512">
|
||||||
|
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
||||||
|
</SVGIcon>
|
||||||
13
src/components/icons/IconChevronRight.astro
Normal file
13
src/components/icons/IconChevronRight.astro
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 320 512">
|
||||||
|
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconFavorites.astro
Normal file
15
src/components/icons/IconFavorites.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 576 512">
|
||||||
|
<path
|
||||||
|
d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconHome.astro
Normal file
15
src/components/icons/IconHome.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 576 512">
|
||||||
|
<path
|
||||||
|
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconInformation.astro
Normal file
15
src/components/icons/IconInformation.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
|
||||||
|
<path
|
||||||
|
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconMoon.astro
Normal file
15
src/components/icons/IconMoon.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 384 512">
|
||||||
|
<path
|
||||||
|
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconReblogs.astro
Normal file
15
src/components/icons/IconReblogs.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
|
||||||
|
<path
|
||||||
|
d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconSquareRSS.astro
Normal file
15
src/components/icons/IconSquareRSS.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 448 512">
|
||||||
|
<path
|
||||||
|
d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconSun.astro
Normal file
15
src/components/icons/IconSun.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
|
||||||
|
<path
|
||||||
|
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
15
src/components/icons/IconTriangleExclamation.astro
Normal file
15
src/components/icons/IconTriangleExclamation.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import SVGIcon from "./SVGIcon.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<SVGIcon {...Astro.props} viewBox="0 0 512 512">
|
||||||
|
<path
|
||||||
|
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||||
|
></path>
|
||||||
|
</SVGIcon>
|
||||||
16
src/components/icons/SVGIcon.astro
Normal file
16
src/components/icons/SVGIcon.astro
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
viewBox: string;
|
||||||
|
class?: string;
|
||||||
|
fill?: string;
|
||||||
|
children: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { width, height, class: className, fill = "currentColor", viewBox } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<svg style={{ width, height, fill }} class={className} viewBox={viewBox} aria-hidden="true">
|
||||||
|
<slot />
|
||||||
|
</svg>
|
||||||
|
|
@ -4,6 +4,10 @@ import BaseLayout from "./BaseLayout.astro";
|
||||||
import Navigation from "../components/Navigation.astro";
|
import Navigation from "../components/Navigation.astro";
|
||||||
import logoBM from "../assets/images/logo_bm.png";
|
import logoBM from "../assets/images/logo_bm.png";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import IconHome from "../components/icons/IconHome.astro";
|
||||||
|
import IconSquareRSS from "../components/icons/IconSquareRSS.astro";
|
||||||
|
import IconSun from "../components/icons/IconSun.astro";
|
||||||
|
import IconMoon from "../components/icons/IconMoon.astro";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
|
|
@ -28,16 +32,16 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
class="flex min-h-screen flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
|
class="flex min-h-screen flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="static mb-4 flex flex-col items-center bg-bm-300 pt-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
|
class="h-card static mb-4 flex flex-col items-center bg-bm-300 pt-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
loading="eager"
|
loading="eager"
|
||||||
src={logo.src}
|
src={logo.src}
|
||||||
alt="Logo for Bad Manners"
|
alt="Logo for Bad Manners"
|
||||||
class="my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md"
|
class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md"
|
||||||
width={192}
|
width={192}
|
||||||
/>
|
/>
|
||||||
<span class="my-2 text-2xl font-semibold">Bad Manners</span>
|
<span class="p-name my-2 text-2xl font-semibold">Bad Manners</span>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div class="pt-4 text-center text-xs text-black dark:text-white">
|
<div class="pt-4 text-center text-xs text-black dark:text-white">
|
||||||
<span
|
<span
|
||||||
|
|
@ -54,28 +58,12 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a>
|
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex items-center gap-x-1 pb-10">
|
<div class="mt-2 flex items-center gap-x-1 pb-10">
|
||||||
<a class="text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelledby="label-main-website">
|
<a class="u-url text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelledby="label-main-website">
|
||||||
<svg
|
<IconHome width="1.5rem" height="1.5rem" />
|
||||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
|
||||||
viewBox="0 0 576 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span id="label-main-website" class="sr-only">Main website</span>
|
<span id="label-main-website" class="sr-only">Main website</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
|
<a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
|
||||||
<svg
|
<IconSquareRSS width="1.5rem" height="1.5rem" />
|
||||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
|
||||||
viewBox="0 0 448 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span id="label-rss-feed" class="sr-only">RSS feed</span>
|
<span id="label-rss-feed" class="sr-only">RSS feed</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
|
@ -84,26 +72,8 @@ const currentYear = new Date().getFullYear().toString();
|
||||||
class="text-link p-1"
|
class="text-link p-1"
|
||||||
aria-labelledby="label-toggle-dark-mode"
|
aria-labelledby="label-toggle-dark-mode"
|
||||||
>
|
>
|
||||||
<svg
|
<IconSun width="1.5rem" height="1.5rem" class="hidden dark:block" />
|
||||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
<IconMoon width="1.5rem" height="1.5rem" class="block dark:hidden" />
|
||||||
viewBox="0 0 512 512"
|
|
||||||
class="hidden dark:block"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
class="block dark:hidden"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span id="label-toggle-dark-mode" class="sr-only">{t("en", "published_content/toggle_dark_mode")}</span>
|
<span id="label-toggle-dark-mode" class="sr-only">{t("en", "published_content/toggle_dark_mode")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,13 @@ import Prose from "../components/Prose.astro";
|
||||||
import MastodonComments from "../components/MastodonComments.astro";
|
import MastodonComments from "../components/MastodonComments.astro";
|
||||||
import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
|
import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
|
||||||
import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
|
import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
|
||||||
|
import IconSun from "../components/icons/IconSun.astro";
|
||||||
|
import IconMoon from "../components/icons/IconMoon.astro";
|
||||||
|
import IconInformation from "../components/icons/IconInformation.astro";
|
||||||
|
import IconArrowBack from "../components/icons/IconArrowBack.astro";
|
||||||
|
import IconChevronLeft from "../components/icons/IconChevronLeft.astro";
|
||||||
|
import IconChevronRight from "../components/icons/IconChevronRight.astro";
|
||||||
|
import IconArrowUp from "../components/icons/IconArrowUp.astro";
|
||||||
|
|
||||||
interface RelatedContent {
|
interface RelatedContent {
|
||||||
link: string;
|
link: string;
|
||||||
|
|
@ -117,15 +124,7 @@ const thumbnail =
|
||||||
class="text-link my-1 p-2"
|
class="text-link my-1 p-2"
|
||||||
aria-labelledby="label-return-to"
|
aria-labelledby="label-return-to"
|
||||||
>
|
>
|
||||||
<svg
|
<IconArrowBack width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only" id="label-return-to"
|
<span class="sr-only" id="label-return-to"
|
||||||
>{
|
>{
|
||||||
series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title
|
series ? t(props.lang, "published_content/return_to_series", series.data.name) : props.labelReturnTo.title
|
||||||
|
|
@ -137,15 +136,7 @@ const thumbnail =
|
||||||
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-labelledby="label-go-to-description"
|
aria-labelledby="label-go-to-description"
|
||||||
>
|
>
|
||||||
<svg
|
<IconInformation width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only" id="label-go-to-description"
|
<span class="sr-only" id="label-go-to-description"
|
||||||
>{t(props.lang, "published_content/go_to_description")}</span
|
>{t(props.lang, "published_content/go_to_description")}</span
|
||||||
>
|
>
|
||||||
|
|
@ -156,26 +147,8 @@ const thumbnail =
|
||||||
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-labelledby="label-toggle-dark-mode"
|
aria-labelledby="label-toggle-dark-mode"
|
||||||
>
|
>
|
||||||
<svg
|
<IconSun width="1.25rem" height="1.25rem" class="hidden dark:block" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
<IconMoon width="1.25rem" height="1.25rem" class="block dark:hidden" />
|
||||||
viewBox="0 0 512 512"
|
|
||||||
class="hidden dark:block"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
class="block dark:hidden"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
<span class="sr-only" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,14 +168,7 @@ const thumbnail =
|
||||||
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
||||||
aria-label={props.labelPreviousContent}
|
aria-label={props.labelPreviousContent}
|
||||||
>
|
>
|
||||||
<svg
|
<IconChevronLeft width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
class="mr-1"
|
|
||||||
viewBox="0 0 320 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
|
||||||
</svg>
|
|
||||||
<span>{props.prev.title}</span>
|
<span>{props.prev.title}</span>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -215,14 +181,7 @@ const thumbnail =
|
||||||
aria-label={props.labelNextContent}
|
aria-label={props.labelNextContent}
|
||||||
>
|
>
|
||||||
<span>{props.next.title}</span>
|
<span>{props.next.title}</span>
|
||||||
<svg
|
<IconChevronRight width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
class="ml-1"
|
|
||||||
viewBox="0 0 320 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div aria-hidden="true" />
|
<div aria-hidden="true" />
|
||||||
|
|
@ -327,17 +286,10 @@ const thumbnail =
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<div class="pr-3 text-right print:hidden">
|
<div class="pr-3 text-right print:hidden">
|
||||||
<a href="#top" class="text-link inline-flex items-center underline" aria-labelledby="label-to-top"
|
<a href="#top" class="text-link inline-flex items-center underline" aria-labelledby="label-to-top">
|
||||||
><svg
|
<IconArrowUp width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
<span id="label-to-top">{t(props.lang, "published_content/to_top")}</span>
|
||||||
class="mr-1"
|
</a>
|
||||||
viewBox="0 0 384 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
><path
|
|
||||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
|
||||||
></path></svg
|
|
||||||
><span id="label-to-top">{t(props.lang, "published_content/to_top")}</span></a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
props.prev || props.next ? (
|
props.prev || props.next ? (
|
||||||
|
|
@ -350,14 +302,7 @@ const thumbnail =
|
||||||
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
|
||||||
aria-label={props.labelPreviousContent}
|
aria-label={props.labelPreviousContent}
|
||||||
>
|
>
|
||||||
<svg
|
<IconChevronLeft width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
class="mr-1"
|
|
||||||
viewBox="0 0 320 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
|
|
||||||
</svg>
|
|
||||||
<span>{props.prev.title}</span>
|
<span>{props.prev.title}</span>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -370,14 +315,7 @@ const thumbnail =
|
||||||
aria-label={props.labelNextContent}
|
aria-label={props.labelNextContent}
|
||||||
>
|
>
|
||||||
<span>{props.next.title}</span>
|
<span>{props.next.title}</span>
|
||||||
<svg
|
<IconChevronRight width="1.25rem" height="1.25rem" />
|
||||||
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
|
|
||||||
class="ml-1"
|
|
||||||
viewBox="0 0 320 512"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue