From 328e84ccc7dc677844b08e3dadfa23348324810f Mon Sep 17 00:00:00 2001 From: Bad Manners <me@badmanners.xyz> Date: Wed, 25 Sep 2024 13:38:04 -0300 Subject: [PATCH] Migrate some scripts to Alpine and add 18+ icon --- astro.config.mjs | 58 ++++---- package-lock.json | 43 ++++++ package.json | 3 + src/components/AgeRestrictedModal.astro | 136 ++++++------------ .../AgeRestrictedScriptInline.astro | 2 +- src/components/DarkModeScript.astro | 35 ----- src/components/DarkModeScriptInline.astro | 2 +- src/components/NoteTooltip.astro | 6 +- src/components/icons/IconNoOneUnder18.astro | 20 +++ src/components/icons/index.ts | 1 + src/content/blog/taken-in-breakdown.mdx | 2 +- src/data/licenses.toml | 11 ++ src/layouts/BaseLayout.astro | 21 ++- src/layouts/GalleryLayout.astro | 3 +- src/layouts/PublishedContentLayout.astro | 3 +- src/pages/blog/index.astro | 7 +- src/pages/games/index.astro | 7 +- src/pages/index.astro | 11 +- src/pages/stories/[...page].astro | 7 +- src/pages/tags/[slug].astro | 16 +++ 20 files changed, 220 insertions(+), 174 deletions(-) delete mode 100644 src/components/DarkModeScript.astro create mode 100644 src/components/icons/IconNoOneUnder18.astro diff --git a/astro.config.mjs b/astro.config.mjs index 7ea28ac..9db12e6 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,39 +6,35 @@ import htaccessIntegration from "astro-htaccess"; import pagefindIntegration from "astro-pagefind"; import { AI_BOTS } from "./src/data/ai_bots"; +import alpinejs from "@astrojs/alpinejs"; + // https://astro.build/config export default defineConfig({ site: "https://gallery.badmanners.xyz", - integrations: [ - tailwindIntegration({ - applyBaseStyles: false, - }), - markdownIntegration(), - mdxIntegration(), - htaccessIntegration({ - generateHtaccessFile: import.meta.env.APACHE_CONFIG === "true", - customRules: [ - // Block AI bots - "<IfModule mod_rewrite.c>", - " RewriteEngine on", - " RewriteBase /", - ` RewriteCond %{HTTP_USER_AGENT} ${AI_BOTS.map((bot) => `^${bot}$`).join("|")} [NC]`, - " RewriteRule ^ – [F]", - "</IfModule>", - ], - redirects: [ - { - match: "/story/", - url: "/stories/", - }, - { - match: "/game/", - url: "/games/", - }, - ], - }), - pagefindIntegration(), - ], + integrations: [tailwindIntegration({ + applyBaseStyles: false, + }), markdownIntegration(), mdxIntegration(), htaccessIntegration({ + generateHtaccessFile: import.meta.env.APACHE_CONFIG === "true", + customRules: [ + // Block AI bots + "<IfModule mod_rewrite.c>", + " RewriteEngine on", + " RewriteBase /", + ` RewriteCond %{HTTP_USER_AGENT} ${AI_BOTS.map((bot) => `^${bot}$`).join("|")} [NC]`, + " RewriteRule ^ – [F]", + "</IfModule>", + ], + redirects: [ + { + match: "/story/", + url: "/stories/", + }, + { + match: "/game/", + url: "/games/", + }, + ], + }), pagefindIntegration(), alpinejs()], markdown: { smartypants: false, shikiConfig: { @@ -67,4 +63,4 @@ export default defineConfig({ }, }, }, -}); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f584577..8e10fb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "1.10.1", "hasInstallScript": true, "dependencies": { + "@astrojs/alpinejs": "^0.4.0", "@astrojs/check": "^0.9.3", "@astrojs/mdx": "^3.1.6", "@astrojs/rss": "^4.0.7", "@astrojs/tailwind": "^5.1.0", "@astropub/md": "^1.0.0", "@tailwindcss/typography": "^0.5.15", + "@types/alpinejs": "^3.13.10", + "alpinejs": "^3.14.1", "astro": "^4.15.5", "astro-htaccess": "^0.2.0", "astro-pagefind": "^1.6.0", @@ -69,6 +72,16 @@ "node": ">=6.0.0" } }, + "node_modules/@astrojs/alpinejs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/alpinejs/-/alpinejs-0.4.0.tgz", + "integrity": "sha512-68BY1CA0XuielLW3WdX2sfh9F4sSTnFqQ//IE9AditbiYJ77HJDb4uZx07pTFDtr1jOMU7lCvH+iS9gmNafM1g==", + "license": "MIT", + "peerDependencies": { + "@types/alpinejs": "^3.0.0", + "alpinejs": "^3.0.0" + } + }, "node_modules/@astrojs/check": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.3.tgz", @@ -1967,6 +1980,12 @@ "@types/estree": "*" } }, + "node_modules/@types/alpinejs": { + "version": "3.13.10", + "resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.10.tgz", + "integrity": "sha512-ah53tF6mWuuwerpDE7EHwbZErNDJQlsLISPqJhYj2RZ9nuTYbRknSkqebUd3igkhLIZKkPa7IiXjSn9qsU9O2w==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2205,6 +2224,21 @@ "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", "license": "MIT" }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -2242,6 +2276,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/alpinejs": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz", + "integrity": "sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", diff --git a/package.json b/package.json index dfff505..539ed0d 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,15 @@ "deploy-lftp": "dotenv tsx scripts/deploy-lftp.ts --" }, "dependencies": { + "@astrojs/alpinejs": "^0.4.0", "@astrojs/check": "^0.9.3", "@astrojs/mdx": "^3.1.6", "@astrojs/rss": "^4.0.7", "@astrojs/tailwind": "^5.1.0", "@astropub/md": "^1.0.0", "@tailwindcss/typography": "^0.5.15", + "@types/alpinejs": "^3.13.10", + "alpinejs": "^3.14.1", "astro": "^4.15.5", "astro-htaccess": "^0.2.0", "astro-pagefind": "^1.6.0", diff --git a/src/components/AgeRestrictedModal.astro b/src/components/AgeRestrictedModal.astro index 56930bd..c20f6e1 100644 --- a/src/components/AgeRestrictedModal.astro +++ b/src/components/AgeRestrictedModal.astro @@ -3,100 +3,54 @@ import AgeRestrictedScriptInline from "./AgeRestrictedScriptInline.astro"; import { IconTriangleExclamation } from "./icons"; --- -<div - id="modal-age-restricted" - class="fixed inset-0 bg-stone-100 dark:bg-stone-900" - role="dialog" - aria-labelledby="title-age-restricted" - hidden -> - <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"> - <IconTriangleExclamation width="3rem" height="3rem" /> - </div> - <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 - </div> - <div - class="mx-6 mb-4 max-w-xl border-b border-stone-300 pb-4 text-xl text-stone-700 dark:border-stone-300 dark:text-stone-50" - > - You must be 18+ to access this page. - </div> - <p class="px-8 text-lg font-light leading-snug text-stone-700 sm:max-w-2xl dark:text-stone-50"> - By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent this - screen from appearing in the future. - </p> - <div - id="age-verification-button-list" - class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 sm:max-w-2xl sm:flex-row" - hidden - > - <button - data-modal-reject - id="age-verification-reject" - class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-stone-300 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50" +<template x-if="!ageVerified"> + <div + id="modal-age-restricted" + class="fixed inset-0 bg-stone-100 dark:bg-stone-900" + role="dialog" + 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="text-bm-500 dark:text-bm-400"> + <IconTriangleExclamation width="3rem" height="3rem" /> + </div> + <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" > - Cancel - </button> - <button - data-modal-accept - id="age-verification-accept" - class="rounded bg-bm-500 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-bm-400 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50" + Age verification + </div> + <div + class="mx-6 mb-4 max-w-xl border-b border-stone-300 pb-4 text-xl text-stone-700 dark:border-stone-300 dark:text-stone-50" > - I'm at least 18 years old - </button> + You must be 18+ to access this page. + </div> + <p class="px-8 text-lg font-light leading-snug text-stone-700 sm:max-w-2xl dark:text-stone-50"> + By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent this + screen from appearing in the future. + </p> + <div + id="age-verification-button-list" + class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 sm:max-w-2xl sm:flex-row" + hidden + > + <button + id="age-verification-reject" + class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-stone-300 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50" + @click="location.href = 'about:blank'" + > + Cancel + </button> + <button + id="age-verification-accept" + class="rounded bg-bm-500 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-bm-400 dark:text-stone-900 dark:hover:bg-stone-600 dark:hover:text-stone-50 dark:focus:bg-stone-600 dark:focus:text-stone-50" + @click="localStorage.ageVerified = 'true'; ageVerified = true" + > + I'm at least 18 years old + </button> + </div> </div> </div> -</div> +</template> <AgeRestrictedScriptInline /> - -<script> - const ageRestrictedModalSetup = () => { - const modal = document.querySelector<HTMLElementTagNameMap["div"]>("div#modal-age-restricted"); - if (!modal) { - // Not an age-restricted page - return; - } - if (modal !== document.querySelector("body>div#modal-age-restricted")) { - throw new Error("#modal-age-restricted must be a direct child of the body element!"); - } - const addAgeVerifiedQueryToLinks = () => - document.body.querySelectorAll<HTMLElementTagNameMap["a"]>("a[href][data-age-restricted]").forEach((el) => { - let newHref = new URL(el.href); - newHref.searchParams.set("ageVerified", "true"); - el.href = newHref.toString(); - }); - if (localStorage.ageVerified === "true") { - addAgeVerifiedQueryToLinks(); - } else { - const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!; - const onRejectButtonClick = (e: MouseEvent) => { - e.preventDefault(); - location.href = "about:blank"; - }; - rejectButton.addEventListener("click", onRejectButtonClick); - const acceptButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!; - acceptButton.addEventListener( - "click", - (e: MouseEvent) => { - e.preventDefault(); - rejectButton.removeEventListener("click", onRejectButtonClick); - localStorage.ageVerified = "true"; - document.body.style.overflow = "auto"; - document.querySelectorAll("body>:not(#modal-age-restricted)").forEach((el) => el.removeAttribute("inert")); - modal.hidden = true; - addAgeVerifiedQueryToLinks(); - }, - { once: true }, - ); - modal.querySelector<HTMLElementTagNameMap["div"]>("div#age-verification-button-list")!.hidden = false; - rejectButton.focus(); - } - }; - - ageRestrictedModalSetup(); -</script> diff --git a/src/components/AgeRestrictedScriptInline.astro b/src/components/AgeRestrictedScriptInline.astro index 8a6164a..87c3d3f 100644 --- a/src/components/AgeRestrictedScriptInline.astro +++ b/src/components/AgeRestrictedScriptInline.astro @@ -1,4 +1,4 @@ --- --- -<script is:inline>(a=>{let b="body>",c="#modal-age-restricted",d="true",e="ageVerified",f="searchParams",g=localStorage,h=new URL(location),i=x=>a.querySelectorAll(x),j=i(b+c)[0];h[f].get(e)==d&&(g[e]=d,h[f].delete(e),history.replaceState({},"",h));j&&g[e]!=d&&((a.body.style.overflow="hidden"),i(b+":not("+c+")").forEach(x=>x.setAttribute("inert",d)),(j.hidden=!1))})(document)</script> +<script is:inline>(()=>{let a="true",b="ageVerified",c="searchParams",d=localStorage,e=new URL(location);e[c].get(b)==a&&(d[b]=a,e[c].delete(b),history.replaceState({},"",e))})()</script> diff --git a/src/components/DarkModeScript.astro b/src/components/DarkModeScript.astro deleted file mode 100644 index 96655df..0000000 --- a/src/components/DarkModeScript.astro +++ /dev/null @@ -1,35 +0,0 @@ ---- -import DarkModeScriptInline from "./DarkModeScriptInline.astro"; ---- - -<DarkModeScriptInline /> - -<script> - type ColorScheme = "auto" | "dark" | "light" | undefined; - - const colorSchemeSetup = () => { - let colorScheme: ColorScheme = localStorage.colorScheme; - if (!colorScheme || colorScheme === "auto") { - colorScheme = matchMedia("(prefers-color-scheme:dark)").matches ? "dark" : "light"; - } - const toggleColorScheme = (e: MouseEvent) => { - e.preventDefault(); - if (colorScheme === "dark") { - colorScheme = "light"; - document.body.classList.remove("dark"); - } else { - colorScheme = "dark"; - document.body.classList.add("dark"); - } - localStorage.colorScheme = colorScheme; - }; - document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => { - button.addEventListener("click", toggleColorScheme); - button.classList.remove("hidden"); - button.hidden = false; - button.setAttribute("aria-hidden", "false"); - }); - }; - - colorSchemeSetup(); -</script> diff --git a/src/components/DarkModeScriptInline.astro b/src/components/DarkModeScriptInline.astro index 7adce1f..7a8db30 100644 --- a/src/components/DarkModeScriptInline.astro +++ b/src/components/DarkModeScriptInline.astro @@ -1,4 +1,4 @@ --- --- -<script is:inline>(a=>{var b="dark",c="colorScheme",d=localStorage,e=d[c];(e=="auto"||!e?matchMedia("(prefers-color-scheme:dark)").matches:e==b)&&a.body.classList.add(b)})(document)</script> +<script is:inline>let g=document,f=a=>{var b="dark",c=localStorage,d=c.colorScheme;(d!="light"&&(d==b||matchMedia("(prefers-color-scheme:dark)").matches))&&a.body.classList.add(b)};g.addEventListener('astro:before-swap',e=>f(e.newDocument));f(g)</script> diff --git a/src/components/NoteTooltip.astro b/src/components/NoteTooltip.astro index 313cf02..14f55f9 100644 --- a/src/components/NoteTooltip.astro +++ b/src/components/NoteTooltip.astro @@ -1,6 +1,6 @@ --- type Props = { - id: number; + id: number | string; title: string; text: string; }; @@ -12,6 +12,6 @@ const { id, title, text } = Astro.props; <a class="decoration-dotted" id={`note-${id}`} href={`#note-${id}`} title={title} data-tooltip> {text} </a> - <sup>{id}</sup> - <em class="sr-only">({title})</em> + {Number.isInteger(id) ? <sup>{id}</sup> : null} + <em class="sr-only"> ({title})</em> </Fragment> diff --git a/src/components/icons/IconNoOneUnder18.astro b/src/components/icons/IconNoOneUnder18.astro new file mode 100644 index 0000000..30b5456 --- /dev/null +++ b/src/components/icons/IconNoOneUnder18.astro @@ -0,0 +1,20 @@ +--- +import SVGIcon from "./SVGIcon.astro"; + +type Props = { + width: string; + height: string; + class?: string; +}; +--- + +<SVGIcon {...Astro.props} viewBox="0 0 36 36" aria-label="🔞"> + <path fill="#000000" d="M34.999 17.999c0 9.389-7.611 17-17 17s-17-7.611-17-17 7.611-17 17-17 17 7.611 17 17" + ></path><path + fill="#F5F8FA" + d="M9.521 12.245H7.85c-1.358 0-1.924-.991-1.924-1.953 0-.99.707-1.952 1.924-1.952h4.019c1.217 0 1.896.876 1.896 2.007v16.104c0 1.414-.906 2.207-2.122 2.207-1.216 0-2.122-.793-2.122-2.207V12.245zm7.307 10.13c0-2.264 1.245-3.934 3.027-4.895-1.33-.963-2.15-2.265-2.15-4.047 0-3.312 2.745-5.434 6.112-5.434 3.283 0 6.14 2.093 6.14 5.434 0 1.583-.791 3.17-2.178 4.047 1.924.96 3.027 2.715 3.027 4.895 0 3.934-3.197 6.451-6.989 6.451-3.906 0-6.989-2.658-6.989-6.451zm4.413-.283c0 1.443.849 2.832 2.575 2.832 1.612 0 2.576-1.389 2.576-2.832 0-1.783-1.02-2.83-2.576-2.83-1.641 0-2.575 1.246-2.575 2.83zm.538-8.206c0 1.274.736 2.151 2.037 2.151 1.302 0 2.066-.877 2.066-2.151 0-1.217-.736-2.151-2.066-2.151-1.33 0-2.037.934-2.037 2.151z" + ></path><path + fill="#DD2E44" + d="M18 0C8.059 0 0 8.06 0 18c0 9.941 8.059 18 18 18s18-8.059 18-18c0-9.94-8.059-18-18-18zm16 18c0 3.969-1.453 7.592-3.845 10.389L7.612 5.845C10.409 3.453 14.032 2 18 2c8.837 0 16 7.164 16 16zM2 18c0-3.968 1.453-7.591 3.844-10.387l22.543 22.543C25.591 32.548 21.968 34 18 34 9.164 34 2 26.837 2 18z" + ></path> +</SVGIcon> diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index 3bb5d5c..aa72a27 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -10,6 +10,7 @@ export { default as IconGamepad } from "./IconGamepad.astro"; export { default as IconHome } from "./IconHome.astro"; export { default as IconMagnifyingGlass } from "./IconMagnifyingGlass.astro"; export { default as IconMoon } from "./IconMoon.astro"; +export { default as IconNoOneUnder18 } from "./IconNoOneUnder18.astro"; export { default as IconRetweet } from "./IconRetweet.astro"; export { default as IconSquareRSS } from "./IconSquareRSS.astro"; export { default as IconStar } from "./IconStar.astro"; diff --git a/src/content/blog/taken-in-breakdown.mdx b/src/content/blog/taken-in-breakdown.mdx index 16b2c3b..dc04be1 100644 --- a/src/content/blog/taken-in-breakdown.mdx +++ b/src/content/blog/taken-in-breakdown.mdx @@ -39,7 +39,7 @@ count = 0; Going over the story and breaking it down was a fun process, all in all. But this was originally a text document, which I had to completely manually refit into this post you're reading (oof...!). Still, for the sake of posteriority, I think it was worth the effort. -If you're up for this non-linear read, then I hope you enjoy this sneak-peek into my thoughts! Just hover or long-press over the <span class="text-link underline decoration-dotted" title="Just like that!" data-tooltip>links with dotted underlines</span>, and it should show the relevant annotations. Now, without further ado, let me reintroduce... +If you're up for this non-linear read, then I hope you enjoy this sneak-peek into my thoughts! Just hover or long-press over the <NoteTooltip id="example" title="Just like that!" text="links with dotted underlines"/>, and it should show the relevant annotations. Now, without further ado, let me reintroduce... <h2> <NoteTooltip diff --git a/src/data/licenses.toml b/src/data/licenses.toml index 7fa958a..1b58450 100644 --- a/src/data/licenses.toml +++ b/src/data/licenses.toml @@ -101,3 +101,14 @@ items = [ "tags", "triangle-exclamation", ] + +[[attributions]] +title = "Twemoji" +author = "Twitter" +description = "Emoji-based icons." +type = "icons" +source = "https://github.com/twitter/twemoji" +license = { name = "CC-BY-4.0", url = "https://creativecommons.org/licenses/by/4.0/" } +items = [ + "U+1F51E No One Under Eighteen Symbol", +] diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 93436fe..87cb4dc 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,7 +1,7 @@ --- import "../styles/base.css"; import "../styles/fonts.css"; -import DarkModeScript from "@components/DarkModeScript.astro"; +import DarkModeScriptInline from "@components/DarkModeScriptInline.astro"; import AgeRestrictedModal from "@components/AgeRestrictedModal.astro"; type Props = { @@ -35,10 +35,25 @@ const { pageTitle, lang = "en", isAgeRestricted } = Astro.props; href={new URL("feed.xml", Astro.site)} /> <slot name="head" /> + <script> + import Alpine from "alpinejs"; + document.addEventListener("astro:after-preparation", () => { + Alpine.stopObservingMutations(); + }); + document.addEventListener("astro:page-load", () => { + document.dispatchEvent(new Event("alpine:init")); + Alpine.initTree(document.documentElement); + Alpine.startObservingMutations(); + }); + </script> </head> - <body> + <body + :class="!ageRestricted || ageVerified ? 'overflow-auto' : 'overflow-hidden'" + x-effect="darkMode ? $el.classList.add('dark') : $el.classList.remove('dark')" + x-data={`{ darkMode: localStorage.colorScheme != 'light' && (localStorage.colorScheme == 'dark' || matchMedia('(prefers-color-scheme:dark)').matches), ageVerified: new URL(location).searchParams.get('ageVerified') == 'true' || localStorage.ageVerified == 'true', ageRestricted: ${isAgeRestricted} }`} + > <slot /> - <DarkModeScript /> + <DarkModeScriptInline /> {isAgeRestricted ? <AgeRestrictedModal /> : null} </body> </html> diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro index 4354dd3..6f25738 100644 --- a/src/layouts/GalleryLayout.astro +++ b/src/layouts/GalleryLayout.astro @@ -105,10 +105,9 @@ const isCurrentRoute = (path: string) => </li> <li> <button - data-dark-mode - hidden class="text-link group" aria-label={t("en", "published_content/toggle_dark_mode")} + @click="darkMode = !darkMode; localStorage.colorScheme = darkMode ? 'dark' : 'light'" > <IconSun width="1.25rem" height="1.25rem" class="order-1 hidden align-text-top dark:inline" /> <IconMoon width="1.25rem" height="1.25rem" class="order-1 inline align-text-top dark:hidden" /> diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro index e0207b7..d517b1b 100644 --- a/src/layouts/PublishedContentLayout.astro +++ b/src/layouts/PublishedContentLayout.astro @@ -137,10 +137,9 @@ const returnTo = series <IconCircleInfo width="1.25rem" height="1.25rem" /> </a> <button - data-dark-mode - hidden class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700" aria-label={t(props.lang, "published_content/toggle_dark_mode")} + @click="darkMode = !darkMode; localStorage.colorScheme = darkMode ? 'dark' : 'light'" > <IconSun width="1.25rem" height="1.25rem" class="hidden dark:block" /> <IconMoon width="1.25rem" height="1.25rem" class="block dark:hidden" /> diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index b5d319e..0cf7534 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -4,7 +4,7 @@ import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import UserComponent from "@components/UserComponent.astro"; import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; -import { IconSquareRSS } from "@components/icons"; +import { IconNoOneUnder18, IconSquareRSS } from "@components/icons"; type PostWithPubDate = CollectionEntry<"blog"> & { data: { pubDate: Date } }; @@ -55,6 +55,11 @@ const posts = await Promise.all( {post.data.title} </span> <br /> + {post.data.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <time class="dt-published italic" datetime={post.data.pubDate.toISOString().slice(0, 10)} diff --git a/src/pages/games/index.astro b/src/pages/games/index.astro index 900bc91..0fb828d 100644 --- a/src/pages/games/index.astro +++ b/src/pages/games/index.astro @@ -4,7 +4,7 @@ import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import { t } from "@i18n"; import UserComponent from "@components/UserComponent.astro"; -import { IconSquareRSS } from "@components/icons"; +import { IconNoOneUnder18, IconSquareRSS } from "@components/icons"; type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } }; @@ -55,6 +55,11 @@ const games = await Promise.all( {game.data.title} </span> <br /> + {game.data.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <time class="dt-published italic" datetime={game.data.pubDate.toISOString().slice(0, 10)} diff --git a/src/pages/index.astro b/src/pages/index.astro index 4b0ccf4..151ba87 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -6,12 +6,13 @@ import GalleryLayout from "@layouts/GalleryLayout.astro"; import { t, type Lang } from "@i18n"; import UserComponent from "@components/UserComponent.astro"; import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; -import { IconSquareRSS } from "@components/icons"; +import { IconNoOneUnder18, IconSquareRSS } from "@components/icons"; const MAX_ITEMS = 10; interface LatestItemsEntry { type: string; + isAgeRestricted: boolean; thumbnail?: ImageMetadata; href: string; title: string; @@ -49,6 +50,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all( fn: async () => ({ type: "Story", + isAgeRestricted: story.data.isAgeRestricted, thumbnail: story.data.thumbnail, href: `/stories/${story.slug}`, title: story.data.title, @@ -63,6 +65,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all( fn: async () => ({ type: "Game", + isAgeRestricted: game.data.isAgeRestricted, thumbnail: game.data.thumbnail, href: `/games/${game.slug}`, title: game.data.title, @@ -77,6 +80,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all( fn: async () => ({ type: "Blog post", + isAgeRestricted: post.data.isAgeRestricted, thumbnail: post.data.thumbnail, href: `/blog/${post.slug}`, title: post.data.title, @@ -140,6 +144,11 @@ const latestItems: LatestItemsEntry[] = await Promise.all( </span> <br /> <span class="italic"> + {entry.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <span class="p-category" aria-label="Category"> {entry.type} </span>{" "} diff --git a/src/pages/stories/[...page].astro b/src/pages/stories/[...page].astro index 9e2ab30..149ce04 100644 --- a/src/pages/stories/[...page].astro +++ b/src/pages/stories/[...page].astro @@ -5,7 +5,7 @@ import { getCollection, getEntries, type CollectionEntry } from "astro:content"; import GalleryLayout from "@layouts/GalleryLayout.astro"; import { t } from "@i18n"; import UserComponent from "@components/UserComponent.astro"; -import { IconSquareRSS } from "@components/icons"; +import { IconNoOneUnder18, IconSquareRSS } from "@components/icons"; type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } }; @@ -111,6 +111,11 @@ const totalPages = Math.ceil(page.total / page.size); {story.data.title} </span> <br /> + {story.data.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <time class="dt-published italic" datetime={story.data.pubDate.toISOString().slice(0, 10)} diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index 538b21c..26ad01d 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -10,6 +10,7 @@ import { qualifyLocalURLsInMarkdown } from "@utils/qualify_local_urls_in_markdow import { markdownToPlaintext } from "@utils/markdown_to_plaintext"; import Prose from "@components/Prose.astro"; import UserComponent from "@components/UserComponent.astro"; +import { IconNoOneUnder18 } from "@components/icons"; type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } }; @@ -179,6 +180,11 @@ const totalWorksWithTag = t( {story.data.title} </span> <br /> + {story.data.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <time class="dt-published italic" datetime={story.data.pubDate.toISOString().slice(0, 10)} @@ -242,6 +248,11 @@ const totalWorksWithTag = t( {game.data.title} </span> <br /> + {game.data.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <time class="dt-published italic" datetime={game.data.pubDate.toISOString().slice(0, 10)} @@ -301,6 +312,11 @@ const totalWorksWithTag = t( {post.data.title} </span> <br /> + {post.data.isAgeRestricted ? ( + <span class="inline-block align-middle" aria-label="Age restricted"> + <IconNoOneUnder18 width="1.25rem" height="1.25rem" /> + </span> + ) : null} <time class="dt-published italic" datetime={post.data.pubDate.toISOString().slice(0, 10)}