Several minor improvements to typing and misc.
- Improved schema validation - Move username parsing and other validators to schema types - Fix astro check command - Add JSON/YAML schema validation for data collections - Update licenses - Remove deployment script in favor of rsync - Prevent unsanitized input in export-story script - Change "eng" language to "en", per BCP47 - Clean up i18n keys and add aria attributes - Improve MastodonComments behavior on no-JS browsers
This commit is contained in:
parent
fe908a4989
commit
7bb8a952ef
54 changed files with 1005 additions and 962 deletions
.vscode
LICENSELICENSE.mdREADME.mdastro.config.mjsexamples
package-lock.jsonpackage.jsonpublic
scripts
src
assets/thumbnails
components
AgeRestrictedModal.astroAuthors.astroCommissioners.astroCopyrightedCharacters.astroDarkModeScript.astroMastodonComments.astroRequesters.astroUserComponent.astro
content
LICENSEconfig.ts
games
stories
tag-categories
1-types-of-vore.yaml2-body-types.yaml3-genders.yaml5-willingness.yaml6-vore-related-scenarios.yaml9-type-of-content.yaml
users
i18n
layouts
pages
utils
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"recommendations": ["astro-build.astro-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
|
"recommendations": [
|
||||||
|
"astro-build.astro-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"redhat.vscode-yaml"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
|
@ -2,6 +2,25 @@
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.css": "tailwindcss"
|
"*.css": "tailwindcss"
|
||||||
},
|
},
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": ["/src/content/series/**"],
|
||||||
|
"url": "./.astro/collections/series.schema.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileMatch": ["/src/content/tag-categories/**"],
|
||||||
|
"url": "./.astro/collections/tag-categories.schema.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileMatch": ["/src/content/users/**"],
|
||||||
|
"url": "./.astro/collections/users.schema.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaml.schemas": {
|
||||||
|
"./.astro/collections/series.schema.json": "/src/content/series/**",
|
||||||
|
"./.astro/collections/tag-categories.schema.json": "/src/content/tag-categories/**",
|
||||||
|
"./.astro/collections/users.schema.json": "/src/content/users/**"
|
||||||
|
},
|
||||||
"[astro]": {
|
"[astro]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 Bad Manners
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
1
LICENSE.md
Normal file
1
LICENSE.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
See [public/licenses.txt](public/licenses.txt)
|
19
README.md
19
README.md
|
@ -5,7 +5,7 @@ Static website built in Astro + Typescript + TailwindCSS.
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js 20+
|
- Node.js 20+
|
||||||
- (optional) LFTP, for the remote deployment script.
|
- (optional) rsync, for remote deployment.
|
||||||
- (optional) LibreOffice, for the story export script.
|
- (optional) LibreOffice, for the story export script.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
@ -31,7 +31,7 @@ npm run prettier # Prettier formatting
|
||||||
Requires `libreoffice` to be installed and in your path.
|
Requires `libreoffice` to be installed and in your path.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-story-to-export
|
npm run export-story -- -o ~/Documents/TO_UPLOAD slug-for-story-to-export
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build and deploy to remote
|
### Build and deploy to remote
|
||||||
|
@ -40,19 +40,8 @@ npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-story-to-exp
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, if you're using LFTP:
|
Then, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`:
|
||||||
|
|
||||||
1. Create a new `.env` file at the root of the project with your credentials (SSH, SFTP, WebDav, etc.) if you haven't already:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DEPLOY_LFTP_HOST=https://example-webdav-server.com
|
|
||||||
DEPLOY_LFTP_USER=example_user
|
|
||||||
DEPLOY_LFTP_PASSWORD=sup3r_s3cr3t_password
|
|
||||||
DEPLOY_LFTP_TARGETFOLDER=sites/gallery.badmanners.xyz/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the deploy command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run deploy-lftp
|
rsync --delete -acP dist/ gallerybm:/home/public
|
||||||
```
|
```
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
assets: "assets",
|
assets: "assets",
|
||||||
},
|
},
|
||||||
|
outDir: "./dist",
|
||||||
redirects: {
|
redirects: {
|
||||||
"/stories": "/stories/1",
|
"/stories": "/stories/1",
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ tags: []
|
||||||
# series: the-lost-of-the-marshes
|
# series: the-lost-of-the-marshes
|
||||||
# relatedStories: []
|
# relatedStories: []
|
||||||
# relatedGames: []
|
# relatedGames: []
|
||||||
# lang: eng
|
# lang: en
|
||||||
---
|
---
|
||||||
|
|
||||||
The game content (i.e. embed) goes here.
|
The game content (i.e. embed) goes here.
|
||||||
|
|
|
@ -25,7 +25,7 @@ tags: []
|
||||||
# Some funny summary
|
# Some funny summary
|
||||||
# relatedStories: []
|
# relatedStories: []
|
||||||
# relatedGames: []
|
# relatedGames: []
|
||||||
# lang: eng
|
# lang: en
|
||||||
---
|
---
|
||||||
|
|
||||||
The story goes here.
|
The story goes here.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: Nameless User
|
name: Nameless User
|
||||||
nameLang:
|
nameLang:
|
||||||
eng: Nameless
|
en: Nameless
|
||||||
tok: jan Nenle pi nimi ala
|
tok: jan Nenle pi nimi ala
|
||||||
# avatar: /src/assets/images/logo_bm.png
|
# avatar: /src/assets/images/logo_bm.png
|
||||||
links:
|
links:
|
||||||
|
|
307
package-lock.json
generated
307
package-lock.json
generated
|
@ -1,26 +1,26 @@
|
||||||
{
|
{
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"version": "1.6.0",
|
"version": "1.6.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"version": "1.6.0",
|
"version": "1.6.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.8.2",
|
"@astrojs/check": "^0.9.2",
|
||||||
"@astrojs/rss": "^4.0.7",
|
"@astrojs/rss": "^4.0.7",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astropub/md": "^1.0.0",
|
"@astropub/md": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"astro": "^4.12.2",
|
"astro": "^4.13.1",
|
||||||
"astro-pagefind": "^1.6.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"marked": "^12.0.1",
|
"marked": "^12.0.2",
|
||||||
"pagefind": "^1.1.0",
|
"pagefind": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.7",
|
||||||
"tiny-decode": "^0.1.3",
|
"tiny-decode": "^0.1.3",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"tsx": "^4.16.2"
|
"tsx": "^4.16.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../astro-pagefind/packages/astro-pagefind": {
|
"../astro-pagefind/packages/astro-pagefind": {
|
||||||
|
@ -83,12 +83,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/check": {
|
"node_modules/@astrojs/check": {
|
||||||
"version": "0.8.2",
|
"version": "0.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.2.tgz",
|
||||||
"integrity": "sha512-L0V9dGb2PGvK9Mf3kby99Y+qm7EqxaC9tN1MVCvaqp/3pPPZBadR4XAySHipxXqQsxwJS25WQow8/1kMl1e25g==",
|
"integrity": "sha512-6rWxtJTbd/ctdAlmla0CAvloGaai5IUTG0K21kctJHHGKJKnGH6Xana7m0zNOtHpVPEJi1SgC/TcsN+ltYt0Cg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/language-server": "^2.12.1",
|
"@astrojs/language-server": "^2.13.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"fast-glob": "^3.3.1",
|
"fast-glob": "^3.3.1",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
|
@ -102,9 +102,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/compiler": {
|
"node_modules/@astrojs/compiler": {
|
||||||
"version": "2.9.2",
|
"version": "2.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.10.2.tgz",
|
||||||
"integrity": "sha512-Vpu0Ffsj8SoV+N0DFHlxxOMKHwSC9059Xy/OlG1t6uFYSoJXxkBC2WyF6igO7x10V+8uJrhOxaXr3nA90kJXow==",
|
"integrity": "sha512-bvH+v8AirwpRWCkYJEyWYdc5Cs/BjG2ZTxIJzttHilXgfKJAdW2496KsUQKzf5j2tOHtaHXKKn9hb9WZiBGpEg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/internal-helpers": {
|
"node_modules/@astrojs/internal-helpers": {
|
||||||
|
@ -114,12 +114,12 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/language-server": {
|
"node_modules/@astrojs/language-server": {
|
||||||
"version": "2.12.1",
|
"version": "2.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.13.2.tgz",
|
||||||
"integrity": "sha512-CCibE6XwSmrZEKlPDr48LZJN7NWxOurOJK1yOzqZFMNV8Y6DIqF6s1e60gbNNHMZkthWYBNTPno4Ni/XyviinQ==",
|
"integrity": "sha512-l435EZLKjaUO/6iewJ7xqd3eHf3zAosVWG4woILbxluQcianBoNPepnnqAg7uUriZUaC44ae5v0Q+AfB8UI64g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.9.1",
|
"@astrojs/compiler": "^2.10.2",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
"@volar/kit": "~2.4.0-alpha.15",
|
"@volar/kit": "~2.4.0-alpha.15",
|
||||||
"@volar/language-core": "~2.4.0-alpha.15",
|
"@volar/language-core": "~2.4.0-alpha.15",
|
||||||
|
@ -256,30 +256,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.24.9",
|
"version": "7.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
|
||||||
"integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==",
|
"integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.24.9",
|
"version": "7.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
|
||||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.24.7",
|
||||||
"@babel/generator": "^7.24.9",
|
"@babel/generator": "^7.25.0",
|
||||||
"@babel/helper-compilation-targets": "^7.24.8",
|
"@babel/helper-compilation-targets": "^7.25.2",
|
||||||
"@babel/helper-module-transforms": "^7.24.9",
|
"@babel/helper-module-transforms": "^7.25.2",
|
||||||
"@babel/helpers": "^7.24.8",
|
"@babel/helpers": "^7.25.0",
|
||||||
"@babel/parser": "^7.24.8",
|
"@babel/parser": "^7.25.0",
|
||||||
"@babel/template": "^7.24.7",
|
"@babel/template": "^7.25.0",
|
||||||
"@babel/traverse": "^7.24.8",
|
"@babel/traverse": "^7.25.2",
|
||||||
"@babel/types": "^7.24.9",
|
"@babel/types": "^7.25.2",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"gensync": "^1.0.0-beta.2",
|
"gensync": "^1.0.0-beta.2",
|
||||||
|
@ -304,12 +304,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.24.10",
|
"version": "7.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
|
||||||
"integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==",
|
"integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.24.9",
|
"@babel/types": "^7.25.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^2.5.1"
|
"jsesc": "^2.5.1"
|
||||||
|
@ -331,12 +331,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets": {
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
"version": "7.24.8",
|
"version": "7.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
|
||||||
"integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==",
|
"integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.24.8",
|
"@babel/compat-data": "^7.25.2",
|
||||||
"@babel/helper-validator-option": "^7.24.8",
|
"@babel/helper-validator-option": "^7.24.8",
|
||||||
"browserslist": "^4.23.1",
|
"browserslist": "^4.23.1",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
|
@ -355,43 +355,6 @@
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-environment-visitor": {
|
|
||||||
"version": "7.24.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
|
|
||||||
"integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.24.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-function-name": {
|
|
||||||
"version": "7.24.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz",
|
|
||||||
"integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/template": "^7.24.7",
|
|
||||||
"@babel/types": "^7.24.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-hoist-variables": {
|
|
||||||
"version": "7.24.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz",
|
|
||||||
"integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.24.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-module-imports": {
|
"node_modules/@babel/helper-module-imports": {
|
||||||
"version": "7.24.7",
|
"version": "7.24.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
|
||||||
|
@ -406,16 +369,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-transforms": {
|
"node_modules/@babel/helper-module-transforms": {
|
||||||
"version": "7.24.9",
|
"version": "7.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
|
||||||
"integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==",
|
"integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-environment-visitor": "^7.24.7",
|
|
||||||
"@babel/helper-module-imports": "^7.24.7",
|
"@babel/helper-module-imports": "^7.24.7",
|
||||||
"@babel/helper-simple-access": "^7.24.7",
|
"@babel/helper-simple-access": "^7.24.7",
|
||||||
"@babel/helper-split-export-declaration": "^7.24.7",
|
"@babel/helper-validator-identifier": "^7.24.7",
|
||||||
"@babel/helper-validator-identifier": "^7.24.7"
|
"@babel/traverse": "^7.25.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
@ -446,18 +408,6 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-split-export-declaration": {
|
|
||||||
"version": "7.24.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
|
|
||||||
"integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.24.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.24.8",
|
"version": "7.24.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
|
||||||
|
@ -486,13 +436,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.24.8",
|
"version": "7.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
|
||||||
"integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==",
|
"integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.24.7",
|
"@babel/template": "^7.25.0",
|
||||||
"@babel/types": "^7.24.8"
|
"@babel/types": "^7.25.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
@ -514,10 +464,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.24.8",
|
"version": "7.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
|
||||||
"integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==",
|
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.25.2"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
},
|
},
|
||||||
|
@ -541,16 +494,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-react-jsx": {
|
"node_modules/@babel/plugin-transform-react-jsx": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz",
|
||||||
"integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==",
|
"integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.24.7",
|
"@babel/helper-annotate-as-pure": "^7.24.7",
|
||||||
"@babel/helper-module-imports": "^7.24.7",
|
"@babel/helper-module-imports": "^7.24.7",
|
||||||
"@babel/helper-plugin-utils": "^7.24.7",
|
"@babel/helper-plugin-utils": "^7.24.8",
|
||||||
"@babel/plugin-syntax-jsx": "^7.24.7",
|
"@babel/plugin-syntax-jsx": "^7.24.7",
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
@ -560,33 +513,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
|
||||||
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
|
"integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.24.7",
|
||||||
"@babel/parser": "^7.24.7",
|
"@babel/parser": "^7.25.0",
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.24.8",
|
"version": "7.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
|
||||||
"integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==",
|
"integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.24.7",
|
||||||
"@babel/generator": "^7.24.8",
|
"@babel/generator": "^7.25.0",
|
||||||
"@babel/helper-environment-visitor": "^7.24.7",
|
"@babel/parser": "^7.25.3",
|
||||||
"@babel/helper-function-name": "^7.24.7",
|
"@babel/template": "^7.25.0",
|
||||||
"@babel/helper-hoist-variables": "^7.24.7",
|
"@babel/types": "^7.25.2",
|
||||||
"@babel/helper-split-export-declaration": "^7.24.7",
|
|
||||||
"@babel/parser": "^7.24.8",
|
|
||||||
"@babel/types": "^7.24.8",
|
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
|
@ -595,9 +545,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.24.9",
|
"version": "7.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
|
||||||
"integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
|
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.24.8",
|
"@babel/helper-string-parser": "^7.24.8",
|
||||||
|
@ -1582,9 +1532,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
|
@ -1863,9 +1814,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/core": {
|
"node_modules/@shikijs/core": {
|
||||||
"version": "1.11.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.12.1.tgz",
|
||||||
"integrity": "sha512-Qsn8h15SWgv5TDRoDmiHNzdQO2BxDe86Yq6vIHf5T0cCvmfmccJKIzHtep8bQO9HMBZYCtCBzaXdd1MnxZBPSg==",
|
"integrity": "sha512-biCz/mnkMktImI6hMfMX3H9kOeqsInxWEyCHbSlL8C/2TR1FqfmGxTLRNwYCKsyCyxWLbB8rEqXRVZuyxuLFmA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hast": "^3.0.4"
|
"@types/hast": "^3.0.4"
|
||||||
|
@ -2285,34 +2236,33 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/astro": {
|
"node_modules/astro": {
|
||||||
"version": "4.12.2",
|
"version": "4.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/astro/-/astro-4.13.1.tgz",
|
||||||
"integrity": "sha512-l6OmqlL+FiuSi9x6F+EGZitteOznq1JffOil7st7cdqeMCTEIym4oagI1a6zp6QekliKWEEZWdplGhgh1k1f7Q==",
|
"integrity": "sha512-VnMjAc+ykFsIVjgbu9Mt/EA1fMIcPMXbU89h3ATwGOzSIKDsQH72bDgfJkWiwk6u0OE90GeP5EPhAT28Twf9oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.9.0",
|
"@astrojs/compiler": "^2.10.0",
|
||||||
"@astrojs/internal-helpers": "0.4.1",
|
"@astrojs/internal-helpers": "0.4.1",
|
||||||
"@astrojs/markdown-remark": "5.2.0",
|
"@astrojs/markdown-remark": "5.2.0",
|
||||||
"@astrojs/telemetry": "3.1.0",
|
"@astrojs/telemetry": "3.1.0",
|
||||||
"@babel/core": "^7.24.9",
|
"@babel/core": "^7.25.2",
|
||||||
"@babel/generator": "^7.24.10",
|
"@babel/generator": "^7.25.0",
|
||||||
"@babel/parser": "^7.24.8",
|
"@babel/parser": "^7.25.3",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.24.7",
|
"@babel/plugin-transform-react-jsx": "^7.25.2",
|
||||||
"@babel/traverse": "^7.24.8",
|
"@babel/traverse": "^7.25.3",
|
||||||
"@babel/types": "^7.24.9",
|
"@babel/types": "^7.25.2",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"acorn": "^8.12.1",
|
"acorn": "^8.12.1",
|
||||||
"aria-query": "^5.3.0",
|
"aria-query": "^5.3.0",
|
||||||
"axobject-query": "^4.1.0",
|
"axobject-query": "^4.1.0",
|
||||||
"boxen": "7.1.1",
|
"boxen": "7.1.1",
|
||||||
"chokidar": "^3.6.0",
|
|
||||||
"ci-info": "^4.0.0",
|
"ci-info": "^4.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"common-ancestor-path": "^1.0.1",
|
"common-ancestor-path": "^1.0.1",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.3.6",
|
||||||
"deterministic-object-hash": "^2.0.2",
|
"deterministic-object-hash": "^2.0.2",
|
||||||
"devalue": "^5.0.0",
|
"devalue": "^5.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
|
@ -2330,7 +2280,7 @@
|
||||||
"http-cache-semantics": "^4.1.1",
|
"http-cache-semantics": "^4.1.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
"magic-string": "^0.30.10",
|
"magic-string": "^0.30.11",
|
||||||
"mrmime": "^2.0.0",
|
"mrmime": "^2.0.0",
|
||||||
"ora": "^8.0.1",
|
"ora": "^8.0.1",
|
||||||
"p-limit": "^6.1.0",
|
"p-limit": "^6.1.0",
|
||||||
|
@ -2339,19 +2289,19 @@
|
||||||
"preferred-pm": "^4.0.0",
|
"preferred-pm": "^4.0.0",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"rehype": "^13.0.1",
|
"rehype": "^13.0.1",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.3",
|
||||||
"shiki": "^1.10.3",
|
"shiki": "^1.12.0",
|
||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"tsconfck": "^3.1.1",
|
"tsconfck": "^3.1.1",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vfile": "^6.0.2",
|
"vfile": "^6.0.2",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.3.5",
|
||||||
"vitefu": "^0.2.5",
|
"vitefu": "^0.2.5",
|
||||||
"which-pm": "^3.0.0",
|
"which-pm": "^3.0.0",
|
||||||
"yargs-parser": "^21.1.1",
|
"yargs-parser": "^21.1.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-to-json-schema": "^3.23.1"
|
"zod-to-json-schema": "^3.23.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"astro": "astro.js"
|
"astro": "astro.js"
|
||||||
|
@ -2958,9 +2908,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
|
@ -4270,12 +4220,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.10",
|
"version": "0.30.11",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
|
||||||
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
|
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
|
@ -4295,9 +4245,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||||
"integrity": "sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==",
|
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
|
@ -6336,12 +6287,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shiki": {
|
"node_modules/shiki": {
|
||||||
"version": "1.11.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.12.1.tgz",
|
||||||
"integrity": "sha512-VHD3Q0EBXaaa245jqayBe5zQyMQUdXBFjmGr9MpDaDpAKRMYn7Ff00DM5MLk26UyKjnml3yQ0O2HNX7PtYVNFQ==",
|
"integrity": "sha512-nwmjbHKnOYYAe1aaQyEBHvQymJgfm86ZSS7fT8OaPRr4sbAcBNz7PbfAikMEFSDQ6se2j2zobkXvVKcBOm0ysg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/core": "1.11.1",
|
"@shikijs/core": "1.12.1",
|
||||||
"@types/hast": "^3.0.4"
|
"@types/hast": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6616,9 +6567,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.6",
|
"version": "3.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
||||||
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
|
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
|
@ -6838,9 +6789,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.16.2",
|
"version": "4.16.5",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz",
|
||||||
"integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==",
|
"integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -7122,9 +7073,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
|
||||||
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
|
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
|
@ -7363,9 +7314,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vscode-languageserver-textdocument": {
|
"node_modules/vscode-languageserver-textdocument": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
||||||
"integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==",
|
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vscode-languageserver-types": {
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
@ -7716,9 +7667,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-to-json-schema": {
|
"node_modules/zod-to-json-schema": {
|
||||||
"version": "3.23.1",
|
"version": "3.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz",
|
||||||
"integrity": "sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==",
|
"integrity": "sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.23.3"
|
"zod": "^3.23.3"
|
||||||
|
|
17
package.json
17
package.json
|
@ -1,33 +1,32 @@
|
||||||
{
|
{
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.6.0",
|
"version": "1.6.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "npm run check && astro build",
|
"build": "astro check && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"sync": "astro sync",
|
"sync": "astro sync",
|
||||||
"check": "astro check --minimumSeverity warning",
|
"check": "astro check",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"prettier": "prettier --write .",
|
"prettier": "prettier --write .",
|
||||||
"deploy-lftp": "dotenv tsx scripts/deploy-lftp.ts --",
|
|
||||||
"export-story": "tsx scripts/export-story.ts"
|
"export-story": "tsx scripts/export-story.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.8.2",
|
"@astrojs/check": "^0.9.2",
|
||||||
"@astrojs/rss": "^4.0.7",
|
"@astrojs/rss": "^4.0.7",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astropub/md": "^1.0.0",
|
"@astropub/md": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"astro": "^4.12.2",
|
"astro": "^4.13.1",
|
||||||
"astro-pagefind": "^1.6.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"marked": "^12.0.1",
|
"marked": "^12.0.2",
|
||||||
"pagefind": "^1.1.0",
|
"pagefind": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.7",
|
||||||
"tiny-decode": "^0.1.3",
|
"tiny-decode": "^0.1.3",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
|
@ -39,6 +38,6 @@
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"tsx": "^4.16.2"
|
"tsx": "^4.16.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
import type { APIRoute } from "astro";
|
The source code of this website is licensed under the MIT License: https://opensource.org/license/mit
|
||||||
|
|
||||||
|
The stories and games hosted on the website are copyrighted by me and licensed under CC-BY-NC-ND-4.0: https://creativecommons.org/licenses/by-nc-nd/4.0/
|
||||||
|
|
||||||
const licenses = `
|
|
||||||
The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
|
The briefcase logo and any unattributed characters are copyrighted and trademarked by me.
|
||||||
|
|
||||||
The source code of this website is licensed under the MIT License. The content hosted on it (i.e. stories, games, and respective thumbnails/images) is copyrighted by me and distributed under the CC BY-NC-ND 4.0 license.
|
The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1: https://opensource.org/license/ofl-1-1
|
||||||
|
|
||||||
The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1.
|
The generic SVG icons were created by Font Awesome and are distributed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/
|
||||||
|
|
||||||
The generic SVG icons were created by Font Awesome and are distributed under the CC-BY-4.0 license.
|
|
||||||
|
|
||||||
All third-party trademarks and attributed characters belong to their respective owners, and I'm not affiliated with any of them.
|
All third-party trademarks and attributed characters belong to their respective owners, and I'm not affiliated with any of them.
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const headers = { "Content-Type": "text/plain; charset=utf-8" };
|
|
||||||
|
|
||||||
export const GET: APIRoute = () => {
|
|
||||||
return new Response(licenses, { headers });
|
|
||||||
};
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { spawn } from "node:child_process";
|
|
||||||
import { program, Option } from "commander";
|
|
||||||
|
|
||||||
interface DeployLftpOptions {
|
|
||||||
host: string;
|
|
||||||
user: string;
|
|
||||||
password: string;
|
|
||||||
targetFolder: string;
|
|
||||||
sourceFolder: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deployLftp({ host, user, password, targetFolder, sourceFolder }: DeployLftpOptions) {
|
|
||||||
const process = spawn(
|
|
||||||
"lftp",
|
|
||||||
[
|
|
||||||
"-c",
|
|
||||||
[
|
|
||||||
`open -u ${user},${password} ${host}`,
|
|
||||||
`mirror --reverse --include-glob assets/* --delete --only-missing --no-perms --verbose ${sourceFolder} ${targetFolder}`,
|
|
||||||
`mirror --reverse --exclude-glob assets/* --delete --no-perms --verbose ${sourceFolder} ${targetFolder}`,
|
|
||||||
`bye`,
|
|
||||||
].join("\n"),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
stdio: "inherit",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
process.on("close", (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(`deploy-lftp failed with code ${code}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await program
|
|
||||||
.name("deploy-lftp")
|
|
||||||
.description("Deploy files to remote server with LFTP")
|
|
||||||
.addOption(
|
|
||||||
new Option("-h, --host <hostname>", "Hostname of the LFTP (i.e. WebDav, SCP, SFTP...) remote.").env(
|
|
||||||
"DEPLOY_LFTP_HOST",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addOption(new Option("-u, --user <username>", "Username portion of the LFTP credentials").env("DEPLOY_LFTP_USER"))
|
|
||||||
.addOption(
|
|
||||||
new Option("-p, --password <pass>", "Password portion of the LFTP credentials").env("DEPLOY_LFTP_PASSWORD"),
|
|
||||||
)
|
|
||||||
.addOption(
|
|
||||||
new Option("-t, --target-folder <remoteDir>", "Folder to mirror files to in the LFTP remote").env(
|
|
||||||
"DEPLOY_LFTP_TARGETFOLDER",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addOption(
|
|
||||||
new Option("-s, --source-folder <localDir>", "Folder to read files from in the local machine")
|
|
||||||
.env("DEPLOY_LFTP_SOURCEFOLDER")
|
|
||||||
.default("dist/"),
|
|
||||||
)
|
|
||||||
.action(deployLftp)
|
|
||||||
.parseAsync();
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type ChildProcess, exec, execSync } from "node:child_process";
|
import { type ChildProcess, exec, 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 as pathJoin, normalize } from "node:path";
|
||||||
|
@ -150,7 +150,13 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
await writeFile(pathJoin(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
await writeFile(pathJoin(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*"));
|
||||||
const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
|
const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
|
||||||
await writeFile(pathJoin(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
await writeFile(pathJoin(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
|
||||||
execSync(`libreoffice --convert-to "rtf:Rich Text Format" --outdir ${tempDir} ${pathJoin(tempDir, "temp.txt")}`);
|
spawnSync("libreoffice", [
|
||||||
|
"--convert-to",
|
||||||
|
"rtf:Rich Text Format",
|
||||||
|
"--outdir",
|
||||||
|
tempDir,
|
||||||
|
pathJoin(tempDir, "temp.txt"),
|
||||||
|
]);
|
||||||
const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
|
const rtfText = await readFile(pathJoin(tempDir, "temp.rtf"), "utf-8");
|
||||||
const rtfStyles = getRTFStyles(rtfText);
|
const rtfStyles = getRTFStyles(rtfText);
|
||||||
await writeFile(
|
await writeFile(
|
||||||
|
@ -158,7 +164,6 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
||||||
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
||||||
);
|
);
|
||||||
console.log("Success!");
|
console.log("Success!");
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await program
|
await program
|
||||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 22 KiB After ![]() (image error) Size: 20 KiB ![]() ![]() |
|
@ -54,11 +54,13 @@
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem("ageVerified") !== "true") {
|
if (localStorage.getItem("ageVerified") !== "true") {
|
||||||
document.body.appendChild(
|
document.body.appendChild(
|
||||||
(document.getElementById("template-modal-age-restricted") as HTMLTemplateElement).content.cloneNode(true),
|
document
|
||||||
|
.querySelector<HTMLElementTagNameMap["template"]>("template#template-modal-age-restricted")!
|
||||||
|
.content.cloneNode(true),
|
||||||
);
|
);
|
||||||
const modal = document.querySelector<HTMLDivElement>("body > #modal-age-restricted")!;
|
const modal = document.querySelector<HTMLElementTagNameMap["div"]>("body > div#modal-age-restricted")!;
|
||||||
const rejectButton = modal.querySelector<HTMLButtonElement>("button[data-modal-reject]")!;
|
const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
|
||||||
const acceptButton = modal.querySelector<HTMLButtonElement>("button[data-modal-accept]")!;
|
const acceptButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!;
|
||||||
function onRejectButtonClick(e: MouseEvent) {
|
function onRejectButtonClick(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
location.href = "about:blank";
|
location.href = "about:blank";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { type Lang } from "../content/config";
|
import type { Lang } from "../content/config";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -12,4 +12,12 @@ const authors = Astro.slots.has("default")
|
||||||
: [];
|
: [];
|
||||||
---
|
---
|
||||||
|
|
||||||
{authors.length ? <p id="authors" set:html={t(lang, "story/authors", authors)} /> : null}
|
{
|
||||||
|
authors.length ? (
|
||||||
|
<p
|
||||||
|
id="authors"
|
||||||
|
aria-label={t(lang, "story/authors_aria_label", authors)}
|
||||||
|
set:html={t(lang, "story/authors", authors)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { type Lang } from "../content/config";
|
import type { Lang } from "../content/config";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -12,4 +12,12 @@ const commissioners = Astro.slots.has("default")
|
||||||
: [];
|
: [];
|
||||||
---
|
---
|
||||||
|
|
||||||
{commissioners.length ? <p id="commissioners" set:html={t(lang, "story/commissioned_by", commissioners)} /> : null}
|
{
|
||||||
|
commissioners.length ? (
|
||||||
|
<p
|
||||||
|
id="commissioners"
|
||||||
|
aria-label={t(lang, "story/commissioners_aria_label", commissioners)}
|
||||||
|
set:html={t(lang, "story/commissioned_by", commissioners)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { type Lang } from "../content/config";
|
import type { Lang } from "../content/config";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import UserComponent from "./UserComponent.astro";
|
import UserComponent from "./UserComponent.astro";
|
||||||
import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
|
import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
|
||||||
|
@ -15,7 +15,7 @@ const { copyrightedCharacters, lang } = Astro.props;
|
||||||
|
|
||||||
{
|
{
|
||||||
copyrightedCharacters ? (
|
copyrightedCharacters ? (
|
||||||
<section id="copyrighted-characters">
|
<section id="copyrighted-characters" aria-label={t(lang, "characters/copyrighted_characters_aria_label")}>
|
||||||
<ul>
|
<ul>
|
||||||
{copyrightedCharacters.map(([owner, characterList]) => (
|
{copyrightedCharacters.map(([owner, characterList]) => (
|
||||||
<CopyrightedCharactersItem
|
<CopyrightedCharactersItem
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var colorScheme = localStorage.getItem("colorScheme");
|
let colorScheme = localStorage.getItem("colorScheme");
|
||||||
if (colorScheme == null || colorScheme === "auto") {
|
if (colorScheme == null || colorScheme === "auto") {
|
||||||
colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
}
|
}
|
||||||
document.querySelectorAll("button[data-dark-mode]").forEach(function (button) {
|
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
|
||||||
button.addEventListener("click", function (e) {
|
button.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (colorScheme === "dark") {
|
if (colorScheme === "dark") {
|
||||||
colorScheme = "light";
|
colorScheme = "light";
|
||||||
|
|
|
@ -1,37 +1,47 @@
|
||||||
---
|
---
|
||||||
|
import type { Lang } from "../content/config";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
instance?: string;
|
lang: Lang;
|
||||||
user?: string;
|
link: string;
|
||||||
postId?: string;
|
instance: string;
|
||||||
|
user: string;
|
||||||
|
postId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { instance, user, postId } = Astro.props;
|
const { link, instance, user, postId } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="comment-section"
|
id="comment-section"
|
||||||
class="hidden px-2 font-serif"
|
class="px-2 font-serif"
|
||||||
aria-describedby="title-comment-section"
|
aria-describedby="title-comment-section"
|
||||||
data-instance={instance || ""}
|
data-link={link}
|
||||||
data-user={user || ""}
|
data-instance={instance}
|
||||||
data-post-id={postId || ""}
|
data-user={user}
|
||||||
|
data-post-id={postId}
|
||||||
>
|
>
|
||||||
<h2 id="title-comment-section" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-comment-section" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
Comments
|
Comments
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-stone-800 dark:text-stone-100" id="comments">
|
<div class="text-stone-800 dark:text-stone-100" id="comments">
|
||||||
<button
|
<p class="my-1">
|
||||||
class="mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 underline disabled:bg-bm-400 disabled:no-underline dark:bg-green-800 dark:disabled:bg-green-900"
|
<a class="text-link underline" href={link} target="_blank">View comments on Mastodon</a>
|
||||||
id="load-comments-button"
|
</p>
|
||||||
data-load-comments
|
|
||||||
>
|
|
||||||
<span>Click to load comments</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template id="template-comments-loading">
|
<template id="template-button">
|
||||||
<svg class="-mt-1 mr-1 inline h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
<button
|
||||||
|
class="mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 underline disabled:bg-bm-400 disabled:no-underline dark:bg-green-800 dark:disabled:bg-green-900"
|
||||||
|
id="load-comments-button"
|
||||||
|
>
|
||||||
|
<span>Click to load comments</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="template-button-loading">
|
||||||
|
<svg class="-mt-1 mr-1 inline h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24" aria-hidden>
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<path
|
<path
|
||||||
class="opacity-100"
|
class="opacity-100"
|
||||||
|
@ -45,12 +55,16 @@ const { instance, user, postId } = Astro.props;
|
||||||
<template id="template-comment-box">
|
<template id="template-comment-box">
|
||||||
<div class="my-2 rounded-md border-2 border-stone-400 bg-stone-200 p-2 dark:border-stone-600 dark:bg-stone-800">
|
<div class="my-2 rounded-md border-2 border-stone-400 bg-stone-200 p-2 dark:border-stone-600 dark:bg-stone-800">
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
<a data-author class="text-link flex items-center text-lg hover:underline focus:underline">
|
<a data-author class="text-link flex items-center text-lg hover:underline focus:underline" target="_blank">
|
||||||
<img data-avatar class="mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" />
|
<img data-avatar class="mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" />
|
||||||
<span data-display-name></span>
|
<span data-display-name></span>
|
||||||
</a>
|
</a>
|
||||||
<a data-post-link class="text-link my-1 flex items-center text-sm font-light hover:underline focus:underline">
|
<a
|
||||||
<span class="mr-1" data-publish-date></span>
|
data-post-link
|
||||||
|
class="text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="mr-1" data-publish-date aria-label="Publish date"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div data-content class="prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div>
|
<div data-content class="prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div>
|
||||||
|
@ -72,11 +86,18 @@ const { instance, user, postId } = Astro.props;
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-comment-thread class="-mb-2"></div>
|
<div data-comment-thread class="-mb-2" aria-hidden></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
interface Post {
|
||||||
|
link: string;
|
||||||
|
instance: string;
|
||||||
|
user: string;
|
||||||
|
postId: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Emoji {
|
interface Emoji {
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -104,115 +125,131 @@ const { instance, user, postId } = Astro.props;
|
||||||
emojis: Emoji[];
|
emojis: Emoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
ancestors: Comment[];
|
||||||
|
descendants: Comment[];
|
||||||
|
}
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const replaceEmojis = (text: string, emojis: Emoji[], imgClass: string) =>
|
function replaceEmojis(text: string, emojis: Emoji[]) {
|
||||||
emojis.reduce(
|
return emojis.reduce(
|
||||||
(acc, emoji) =>
|
(acc, emoji) =>
|
||||||
acc.replaceAll(
|
acc.replaceAll(
|
||||||
`:${emoji.shortcode}:`,
|
`:${emoji.shortcode}:`,
|
||||||
`<img class="${imgClass}" alt=":${emoji.shortcode}: emoji" src="${emoji.url}" />`,
|
`<img class="inline mx-[1px] w-5" alt=":${emoji.shortcode}: emoji" src="${emoji.url}" />`,
|
||||||
),
|
),
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const commentSection = document.querySelector<Element>("#comment-section")!;
|
async function renderComments(section: Element, post: Post) {
|
||||||
const instance = commentSection.getAttribute("data-instance");
|
const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
||||||
const user = commentSection.getAttribute("data-user");
|
try {
|
||||||
const postId = commentSection.getAttribute("data-post-id");
|
const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
|
||||||
if (instance && user && postId) {
|
if (!response.ok) {
|
||||||
commentSection.classList.remove("hidden");
|
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
||||||
commentSection.querySelector<HTMLButtonElement>("button[data-load-comments]")!.addEventListener("click", (e) => {
|
}
|
||||||
|
const data: ApiResponse = await response.json();
|
||||||
|
|
||||||
|
const commentsList: HTMLElement[] = [];
|
||||||
|
const commentMap: Record<string, number> = {};
|
||||||
|
const commentTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
|
||||||
|
"template#template-comment-box",
|
||||||
|
)!;
|
||||||
|
data.descendants.forEach((comment) => {
|
||||||
|
const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
|
||||||
|
|
||||||
|
const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
|
||||||
|
commentBoxAuthor.href = comment.account.url;
|
||||||
|
const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!;
|
||||||
|
avatar.src = comment.account.avatar;
|
||||||
|
avatar.alt = `Profile picture of ${comment.account.username}`;
|
||||||
|
const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!;
|
||||||
|
displayName.innerHTML = replaceEmojis(comment.account.display_name, comment.account.emojis);
|
||||||
|
|
||||||
|
const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
|
||||||
|
commentBoxPostLink.href = comment.url;
|
||||||
|
const publishDate =
|
||||||
|
commentBoxPostLink.querySelector<HTMLElementTagNameMap["span"]>("span[data-publish-date]")!;
|
||||||
|
publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment.edited_at) {
|
||||||
|
const edited = document.createElement("span");
|
||||||
|
edited.className = "italic";
|
||||||
|
edited.innerText = "(edited)";
|
||||||
|
commentBoxPostLink.appendChild(edited);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentBoxContent = commentBox.querySelector<HTMLElementTagNameMap["div"]>("div[data-content]")!;
|
||||||
|
commentBoxContent.innerHTML = replaceEmojis(comment.content, comment.emojis);
|
||||||
|
|
||||||
|
const commentBoxFavorites = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-favorites]")!;
|
||||||
|
commentBoxFavorites.innerText = comment.favourites_count.toString();
|
||||||
|
|
||||||
|
const commentBoxReblogs = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-reblogs]")!;
|
||||||
|
commentBoxReblogs.innerText = comment.reblogs_count.toString();
|
||||||
|
|
||||||
|
if (comment.in_reply_to_id === post.postId || !(comment.in_reply_to_id in commentMap)) {
|
||||||
|
commentMap[comment.id] = commentsList.length;
|
||||||
|
commentsList.push(commentBox);
|
||||||
|
} else {
|
||||||
|
const commentsIndex = commentMap[comment.in_reply_to_id];
|
||||||
|
commentMap[comment.id] = commentsIndex;
|
||||||
|
const parentThreadDiv =
|
||||||
|
commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
|
||||||
|
parentThreadDiv.removeAttribute("aria-hidden");
|
||||||
|
parentThreadDiv.setAttribute("aria-label", "Replies");
|
||||||
|
parentThreadDiv.appendChild(commentBox);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (commentsList.length === 0) {
|
||||||
|
commentsDiv.innerHTML = `<p class="my-1">No comments yet. <a class="text-link underline" href="${post.link}" target="_blank">Be the first to join the conversation on Mastodon</a>.</p>`;
|
||||||
|
} else {
|
||||||
|
commentsDiv.innerHTML = `<p class="my-1">Join the conversation <a class="text-link underline" href="${post.link}" target="_blank">by replying on Mastodon</a>.</p>`;
|
||||||
|
commentsDiv.append(...commentsList);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
commentsDiv.innerHTML = `<p class="my-1">Unable to load comments. Please try again later.</p>`;
|
||||||
|
console.error("Fetch Mastodon comments error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCommentSection() {
|
||||||
|
const commentSection = document.querySelector<HTMLElementTagNameMap["section"]>("section#comment-section");
|
||||||
|
if (!commentSection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const post = {
|
||||||
|
link: commentSection.dataset.link,
|
||||||
|
instance: commentSection.dataset.instance,
|
||||||
|
user: commentSection.dataset.user,
|
||||||
|
postId: commentSection.dataset.postId,
|
||||||
|
};
|
||||||
|
if (!post.link || !post.instance || !post.user || !post.postId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loadCommentsButton = document
|
||||||
|
.querySelector<HTMLElementTagNameMap["template"]>("template#template-button")!
|
||||||
|
.content.cloneNode(true) as HTMLButtonElement;
|
||||||
|
commentSection.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!.replaceChildren(loadCommentsButton);
|
||||||
|
loadCommentsButton.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const loadCommentsButton = e.target as HTMLButtonElement;
|
|
||||||
loadCommentsButton.setAttribute("disabled", "true");
|
loadCommentsButton.setAttribute("disabled", "true");
|
||||||
loadCommentsButton.replaceChildren(
|
loadCommentsButton.replaceChildren(
|
||||||
(document.getElementById("template-comments-loading") as HTMLTemplateElement).content.cloneNode(true),
|
document
|
||||||
|
.querySelector<HTMLElementTagNameMap["template"]>("template#template-button-loading")!
|
||||||
|
.content.cloneNode(true),
|
||||||
);
|
);
|
||||||
const renderComments = async () => {
|
renderComments(commentSection, post as Post);
|
||||||
try {
|
|
||||||
if (!instance || !user || !postId) {
|
|
||||||
throw new Error(
|
|
||||||
`Cannot fetch comments without all fields (instance=${instance}, user=${user}, post-id=${postId})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const response = await fetch(`https://${instance}/api/v1/statuses/${postId}/context`);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
|
||||||
}
|
|
||||||
const data: { ancestors: Comment[]; descendants: Comment[] } = await response.json();
|
|
||||||
|
|
||||||
const commentsList: HTMLElement[] = [];
|
|
||||||
const commentMap: Record<string, number> = {};
|
|
||||||
const commentTemplate = document.getElementById("template-comment-box") as HTMLTemplateElement;
|
|
||||||
data.descendants.forEach((comment) => {
|
|
||||||
const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
|
|
||||||
|
|
||||||
const commentBoxAuthor = commentBox.querySelector<HTMLAnchorElement>("[data-author]")!;
|
|
||||||
commentBoxAuthor.href = comment.account.url;
|
|
||||||
commentBoxAuthor.target = "_blank";
|
|
||||||
const avatar = commentBoxAuthor.querySelector<HTMLImageElement>("[data-avatar]")!;
|
|
||||||
avatar.src = comment.account.avatar;
|
|
||||||
avatar.alt = `Profile picture of ${comment.account.username}`;
|
|
||||||
const displayName = commentBoxAuthor.querySelector<HTMLSpanElement>("[data-display-name]")!;
|
|
||||||
displayName.innerHTML = replaceEmojis(
|
|
||||||
comment.account.display_name,
|
|
||||||
comment.account.emojis,
|
|
||||||
"inline mx-[1px] w-5",
|
|
||||||
);
|
|
||||||
|
|
||||||
const commentBoxPostLink = commentBox.querySelector<HTMLAnchorElement>("[data-post-link]")!;
|
|
||||||
commentBoxPostLink.href = comment.url;
|
|
||||||
commentBoxPostLink.target = "_blank";
|
|
||||||
const publishDate = commentBoxPostLink.querySelector<HTMLSpanElement>("[data-publish-date]")!;
|
|
||||||
publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (comment.edited_at) {
|
|
||||||
const edited = document.createElement("span");
|
|
||||||
edited.className = "italic";
|
|
||||||
edited.innerText = "(edited)";
|
|
||||||
commentBoxPostLink.appendChild(edited);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentBoxContent = commentBox.querySelector<HTMLDivElement>("[data-content]")!;
|
|
||||||
commentBoxContent.innerHTML = replaceEmojis(comment.content, comment.emojis, "inline mx-[1px] w-5");
|
|
||||||
|
|
||||||
const commentBoxFavorites = commentBox.querySelector<HTMLSpanElement>("[data-favorites]")!;
|
|
||||||
commentBoxFavorites.innerText = comment.favourites_count.toString();
|
|
||||||
|
|
||||||
const commentBoxReblogs = commentBox.querySelector<HTMLSpanElement>("[data-reblogs]")!;
|
|
||||||
commentBoxReblogs.innerText = comment.reblogs_count.toString();
|
|
||||||
|
|
||||||
if (comment.in_reply_to_id === postId || !(comment.in_reply_to_id in commentMap)) {
|
|
||||||
commentMap[comment.id] = commentsList.length;
|
|
||||||
commentsList.push(commentBox);
|
|
||||||
} else {
|
|
||||||
const commentsIndex = commentMap[comment.in_reply_to_id];
|
|
||||||
commentMap[comment.id] = commentsIndex;
|
|
||||||
commentsList[commentsIndex]
|
|
||||||
.querySelector<HTMLDivElement>("[data-comment-thread]")!
|
|
||||||
.appendChild(commentBox);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const commentsDiv = commentSection.querySelector<HTMLDivElement>("#comments")!;
|
|
||||||
if (commentsList.length === 0) {
|
|
||||||
commentsDiv.innerHTML = `<p class="my-1">No comments yet. <a class="text-link underline" href="https://${instance}/@${user}/${postId}" target="_noblank">Be the first to join the conversation on Mastodon</a>.</p>`;
|
|
||||||
} else {
|
|
||||||
commentsDiv.innerHTML = `<p class="my-1">Join the conversation <a class="text-link underline" href="https://${instance}/@${user}/${postId}" target="_noblank">by replying on Mastodon</a>.</p>`;
|
|
||||||
commentsDiv.append(...commentsList);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadCommentsButton.innerHTML = `<span>Unable to load comments.</span>`;
|
|
||||||
console.error("Fetch Mastodon comments error", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
renderComments();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initCommentSection();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { type Lang } from "../content/config";
|
import type { Lang } from "../content/config";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -12,4 +12,12 @@ const requesters = Astro.slots.has("default")
|
||||||
: [];
|
: [];
|
||||||
---
|
---
|
||||||
|
|
||||||
{requesters.length ? <p id="requesters" set:html={t(lang, "story/requested_by", requesters)} /> : null}
|
{
|
||||||
|
requesters.length ? (
|
||||||
|
<p
|
||||||
|
id="requesters"
|
||||||
|
aria-label={t(lang, "story/requesters_aria_label", requesters)}
|
||||||
|
set:html={t(lang, "story/requested_by", requesters)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { type Lang } from "../content/config";
|
import type { Lang } from "../content/config";
|
||||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -12,12 +12,7 @@ let { user, lang } = Astro.props;
|
||||||
const username = getUsernameForLang(user, lang);
|
const username = getUsernameForLang(user, lang);
|
||||||
let link: string | null = null;
|
let link: string | null = null;
|
||||||
if (user.data.preferredLink) {
|
if (user.data.preferredLink) {
|
||||||
const preferredLink = user.data.links[user.data.preferredLink]!;
|
link = user.data.links[user.data.preferredLink]!.link;
|
||||||
if (typeof preferredLink === "string") {
|
|
||||||
link = preferredLink;
|
|
||||||
} else {
|
|
||||||
link = preferredLink[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../assets/LICENSE
|
|
|
@ -2,89 +2,214 @@ import { defineCollection, reference, z } from "astro:content";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
|
|
||||||
export const WEBSITE_LIST = [
|
export const DEFAULT_LANG = "en";
|
||||||
"website",
|
|
||||||
"eka",
|
|
||||||
"furaffinity",
|
|
||||||
"weasyl",
|
|
||||||
"inkbunny",
|
|
||||||
"sofurry",
|
|
||||||
"mastodon",
|
|
||||||
"twitter",
|
|
||||||
"bluesky",
|
|
||||||
"itaku",
|
|
||||||
] as const;
|
|
||||||
export const GAME_PLATFORMS = ["web", "windows", "linux", "macos", "android", "ios"] as const;
|
|
||||||
export const DEFAULT_LANG = "eng";
|
|
||||||
export const DEFAULT_AUTHOR_ID = "bad-manners";
|
|
||||||
export const ANONYMOUS_USER_ID = "anonymous";
|
export const ANONYMOUS_USER_ID = "anonymous";
|
||||||
|
|
||||||
// Validators
|
|
||||||
|
|
||||||
const ekaPostUrlRegex = /^(?:https:\/\/)(?:www\.)?aryion\.com\/g4\/view\/([1-9]\d*)\/?$/;
|
|
||||||
const furaffinityPostUrlRegex = /^(?:https:\/\/)(?:www\.)?furaffinity\.net\/view\/([1-9]\d*)\/?$/;
|
|
||||||
const weasylPostUrlRegex =
|
|
||||||
/^(?:https:\/\/)(?:www\.)?weasyl\.com\/~([a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/([1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/;
|
|
||||||
const inkbunnyPostUrlRegex = /^(?:https:\/\/)(?:www\.)?inkbunny\.net\/s\/([1-9]\d*)\/?$/;
|
|
||||||
const sofurryPostUrlRegex = /^(?:https:\/\/)www\.sofurry\.com\/view\/([1-9]\d*)\/?$/;
|
|
||||||
const mastodonPostUrlRegex = /^(?:https:\/\/)((?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@([a-zA-Z][a-zA-Z0-9_-]+)\/([1-9]\d*)\/?$/;
|
|
||||||
|
|
||||||
const refineAuthors = [
|
|
||||||
(value: { id: any } | any[]) => "id" in value || value.length > 0,
|
|
||||||
`"authors" cannot be empty`,
|
|
||||||
] as const;
|
|
||||||
const refineCopyrightedCharacters = [
|
|
||||||
(value: Record<string, any>) => !("" in value) || Object.keys(value).length == 1,
|
|
||||||
`"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
// Transformers
|
// Transformers
|
||||||
|
|
||||||
const trimText = (text: string) => text.trim();
|
function parseRegex<R extends { [key: string]: string }>(regex: RegExp) {
|
||||||
const adjustDateForUTCOffset = (date: Date) =>
|
return (url: string, ctx: z.RefinementCtx) => {
|
||||||
new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
|
const match = regex.exec(url);
|
||||||
const parseMastodonPostUrl = (url: string, ctx: z.RefinementCtx) => {
|
if (!match?.groups) {
|
||||||
const match = mastodonPostUrlRegex.exec(url);
|
ctx.addIssue({
|
||||||
if (!match) {
|
code: z.ZodIssueCode.custom,
|
||||||
ctx.addIssue({
|
message: `"${ctx.path}" did not match regex`,
|
||||||
code: z.ZodIssueCode.custom,
|
});
|
||||||
message: `"mastodon" post contains an invalid URL`,
|
return z.NEVER;
|
||||||
});
|
}
|
||||||
return z.NEVER;
|
return match.groups as R;
|
||||||
}
|
|
||||||
return {
|
|
||||||
instance: match[1]!,
|
|
||||||
user: match[2]!,
|
|
||||||
postId: match[3]!,
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Schema definitions
|
||||||
|
|
||||||
|
/** Record of website links for a user.
|
||||||
|
*
|
||||||
|
* For each entry, you can enter a URL for the value or - for any key apart
|
||||||
|
* from `website` - a pre-parsed object containing the link and username.
|
||||||
|
*/
|
||||||
|
const websiteLinks = z.object({
|
||||||
|
website: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.transform((link) => {
|
||||||
|
link;
|
||||||
|
}),
|
||||||
|
eka: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?(?:www\.)?aryion\.com\/g4\/user\/(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
furaffinity: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?(?:www\.)?furaffinity\.net\/user\/(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
weasyl: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?(?:www\.)?weasyl\.com\/\~(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
inkbunny: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?(?:www\.)?inkbunny\.net\/(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
sofurry: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(/^(?:https?:\/\/)?(?<username>[^\.]+).sofurry.com\/?$/)(
|
||||||
|
link,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
mastodon: z
|
||||||
|
.object({ link: z.string().url(), username: z.string().regex(/^[^@]+@[^@]+$/) })
|
||||||
|
.or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username, instance } = parseRegex<{ username: string; instance: string }>(
|
||||||
|
/^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/(?:@|users\/)(?<username>[a-zA-Z][a-zA-Z0-9_-]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username: `${username}@${instance}` };
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.transform(({ link, username }) => {
|
||||||
|
const i = username.indexOf("@");
|
||||||
|
return { link, username, handle: username.slice(0, i), instance: username.slice(i + 1) };
|
||||||
|
}),
|
||||||
|
twitter: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?(?:www\.)?(?:twitter\.com|x\.com)\/@?(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
bluesky: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?bsky\.app\/profile\/(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
itaku: z.object({ link: z.string().url(), username: z.string() }).or(
|
||||||
|
z.string().transform((link, ctx) => {
|
||||||
|
const { username } = parseRegex<{ username: string }>(
|
||||||
|
/^(?:https?:\/\/)?(?:www\.)?itaku\.ee\/profile\/(?<username>[^\/]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, username };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
/** Available languages. See https://r12a.github.io/app-subtags/ */
|
||||||
|
const lang = z.enum(["en", "tok"]).default(DEFAULT_LANG);
|
||||||
|
/** Platforms for a game. */
|
||||||
|
const platform = z.enum(["web", "windows", "linux", "macos", "android", "ios"]);
|
||||||
|
const userList = z
|
||||||
|
.array(reference("users"))
|
||||||
|
.refine((value) => value.length > 0, `user list cannot be empty`)
|
||||||
|
.or(reference("users").transform((user) => [user]));
|
||||||
|
/** A record of the format `{"Character name": "user-id"}`.
|
||||||
|
*
|
||||||
|
* An empty character name `""` indicates that all characters are copyrighted
|
||||||
|
* by a certain user.
|
||||||
|
*/
|
||||||
|
const copyrightedCharacters = z
|
||||||
|
.record(z.string(), reference("users"))
|
||||||
|
.refine(
|
||||||
|
(value) => !("" in value) || Object.keys(value).length == 1,
|
||||||
|
`"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
|
||||||
|
)
|
||||||
|
.default({});
|
||||||
|
/** A record of the format `{ en: string, tok?: string, ... }`. */
|
||||||
|
const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()));
|
||||||
|
/** Common attributes for published content (stories + games). */
|
||||||
|
const publishedContent = z.object({
|
||||||
|
// Required parameters
|
||||||
|
title: z.string(),
|
||||||
|
authors: userList,
|
||||||
|
contentWarning: z.string().trim(),
|
||||||
|
// Required parameters, but optional for drafts (isDraft == true)
|
||||||
|
pubDate: z
|
||||||
|
.date()
|
||||||
|
.transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
|
||||||
|
.optional(),
|
||||||
|
description: z.string().trim(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
// Optional parameters
|
||||||
|
isDraft: z.boolean().default(false),
|
||||||
|
relatedStories: z.array(reference("stories")).default([]),
|
||||||
|
relatedGames: z.array(reference("games")).default([]),
|
||||||
|
lang: lang,
|
||||||
|
copyrightedCharacters: copyrightedCharacters,
|
||||||
|
series: reference("series").optional(),
|
||||||
|
posts: z
|
||||||
|
.object({
|
||||||
|
eka: z.string().transform((link, ctx) => {
|
||||||
|
const { postId } = parseRegex<{ postId: string }>(
|
||||||
|
/^(?:https?:\/\/)(?:www\.)?aryion\.com\/g4\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, postId };
|
||||||
|
}),
|
||||||
|
furaffinity: z.string().transform((link, ctx) => {
|
||||||
|
const { postId } = parseRegex<{ postId: string }>(
|
||||||
|
/^(?:https?:\/\/)(?:www\.)?furaffinity\.net\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, postId };
|
||||||
|
}),
|
||||||
|
weasyl: z.string().transform((link, ctx) => {
|
||||||
|
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||||
|
/^(?:https?:\/\/)(?:www\.)?weasyl\.com\/~(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/(?<postId>[1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, user, postId };
|
||||||
|
}),
|
||||||
|
inkbunny: z.string().transform((link, ctx) => {
|
||||||
|
const { postId } = parseRegex<{ postId: string }>(
|
||||||
|
/^(?:https?:\/\/)(?:www\.)?inkbunny\.net\/s\/(?<postId>[1-9]\d*)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, postId };
|
||||||
|
}),
|
||||||
|
sofurry: z.string().transform((link, ctx) => {
|
||||||
|
const { postId } = parseRegex<{ postId: string }>(
|
||||||
|
/^(?:https?:\/\/)www\.sofurry\.com\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, postId };
|
||||||
|
}),
|
||||||
|
bluesky: z.string().transform((link, ctx) => {
|
||||||
|
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||||
|
/^(?:https?:\/\/)bsky\.app\/profile\/(?<user>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/post\/(?<postId>[a-z0-9]+)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, user, postId };
|
||||||
|
}),
|
||||||
|
mastodon: z.string().transform((link, ctx) => {
|
||||||
|
const { instance, user, postId } = parseRegex<{ instance: string; user: string; postId: string }>(
|
||||||
|
/^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/(?<postId>[1-9]\d*)\/?$/,
|
||||||
|
)(link, ctx);
|
||||||
|
return { link, instance, user, postId };
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.default({}),
|
||||||
|
});
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
const lang = z.enum(["eng", "tok"]).default(DEFAULT_LANG);
|
|
||||||
const website = z.enum(WEBSITE_LIST);
|
|
||||||
const platform = z.enum(GAME_PLATFORMS);
|
|
||||||
const mastodonPost = z
|
|
||||||
.object({
|
|
||||||
instance: z.string(),
|
|
||||||
user: z.string(),
|
|
||||||
postId: z.string(),
|
|
||||||
})
|
|
||||||
.or(z.string().transform(parseMastodonPostUrl));
|
|
||||||
const userList = z
|
|
||||||
.array(reference("users"))
|
|
||||||
.or(reference("users").transform((user) => [user]))
|
|
||||||
.refine((value) => value.length > 0, `user list cannot be empty`);
|
|
||||||
const authors = userList.default([DEFAULT_AUTHOR_ID]);
|
|
||||||
const copyrightedCharacters = z
|
|
||||||
.record(z.string(), reference("users"))
|
|
||||||
.default({})
|
|
||||||
.refine(...refineCopyrightedCharacters);
|
|
||||||
// { eng: string, tok?: string, ... }
|
|
||||||
const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()));
|
|
||||||
|
|
||||||
export type Lang = z.output<typeof lang>;
|
export type Lang = z.output<typeof lang>;
|
||||||
export type Website = z.infer<typeof website>;
|
export type Website = keyof z.input<typeof websiteLinks>;
|
||||||
export type GamePlatform = z.infer<typeof platform>;
|
export type GamePlatform = z.infer<typeof platform>;
|
||||||
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
||||||
|
|
||||||
|
@ -93,79 +218,52 @@ export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
||||||
const storiesCollection = defineCollection({
|
const storiesCollection = defineCollection({
|
||||||
type: "content",
|
type: "content",
|
||||||
schema: ({ image }) =>
|
schema: ({ image }) =>
|
||||||
z.object({
|
z
|
||||||
// Required
|
.object({
|
||||||
title: z.string(),
|
// Required parameters, but optional for drafts (isDraft == true)
|
||||||
wordCount: z.number().int().optional(),
|
wordCount: z.number().int().optional(),
|
||||||
contentWarning: z.string().transform(trimText),
|
thumbnail: image().optional(),
|
||||||
description: z.string().transform(trimText),
|
// Optional parameters
|
||||||
tags: z.array(z.string()),
|
shortTitle: z.string().optional(),
|
||||||
// Optional
|
commissioner: userList.optional(),
|
||||||
pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
|
requester: userList.optional(),
|
||||||
isDraft: z.boolean().default(false),
|
summary: z.string().trim().optional(),
|
||||||
shortTitle: z.string().optional(),
|
thumbnailWidth: z.number().int().optional(),
|
||||||
authors,
|
thumbnailHeight: z.number().int().optional(),
|
||||||
summary: z.string().transform(trimText).optional(),
|
prev: reference("stories").nullish(),
|
||||||
thumbnail: image().optional(),
|
next: reference("stories").nullish(),
|
||||||
thumbnailWidth: z.number().int().optional(),
|
})
|
||||||
thumbnailHeight: z.number().int().optional(),
|
.and(publishedContent)
|
||||||
series: reference("series").optional(),
|
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published story`)
|
||||||
commissioner: userList.optional(),
|
.refine(
|
||||||
requester: userList.optional(),
|
({ isDraft, contentWarning }) => isDraft || contentWarning,
|
||||||
copyrightedCharacters: copyrightedCharacters,
|
`Missing "contentWarning" for published story`,
|
||||||
lang,
|
)
|
||||||
prev: reference("stories").nullish(),
|
.refine(({ isDraft, wordCount }) => isDraft || wordCount, `Missing "wordCount" for published story`)
|
||||||
next: reference("stories").nullish(),
|
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published story`)
|
||||||
relatedStories: z.array(reference("stories")).default([]),
|
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
|
||||||
relatedGames: z.array(reference("games")).default([]),
|
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published story`),
|
||||||
posts: z
|
|
||||||
.object({
|
|
||||||
eka: z.string().regex(ekaPostUrlRegex),
|
|
||||||
furaffinity: z.string().regex(furaffinityPostUrlRegex),
|
|
||||||
weasyl: z.string().regex(weasylPostUrlRegex),
|
|
||||||
inkbunny: z.string().regex(inkbunnyPostUrlRegex),
|
|
||||||
sofurry: z.string().regex(sofurryPostUrlRegex),
|
|
||||||
mastodon: mastodonPost,
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
.default({}),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const gamesCollection = defineCollection({
|
const gamesCollection = defineCollection({
|
||||||
type: "content",
|
type: "content",
|
||||||
schema: ({ image }) =>
|
schema: ({ image }) =>
|
||||||
z.object({
|
z
|
||||||
// Required
|
.object({
|
||||||
title: z.string(),
|
// Required parameters, but optional for drafts (isDraft == true)
|
||||||
contentWarning: z.string().transform(trimText),
|
platforms: z.array(platform).default([]),
|
||||||
description: z.string().transform(trimText),
|
thumbnail: image().optional(),
|
||||||
platforms: z.array(platform).refine((platforms) => platforms.length > 0, `"platforms" cannot be empty`),
|
// Optional parameters
|
||||||
tags: z.array(z.string()),
|
thumbnailWidth: z.number().int().optional(),
|
||||||
// Optional
|
thumbnailHeight: z.number().int().optional(),
|
||||||
pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
|
})
|
||||||
isDraft: z.boolean().default(false),
|
.and(publishedContent)
|
||||||
authors,
|
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published game`)
|
||||||
thumbnail: image().optional(),
|
.refine(({ isDraft, contentWarning }) => isDraft || contentWarning, `Missing "contentWarning" for published game`)
|
||||||
thumbnailWidth: z.number().int().optional(),
|
.refine(({ isDraft, platforms }) => isDraft || platforms.length, `Missing "platforms" for published game`)
|
||||||
thumbnailHeight: z.number().int().optional(),
|
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published game`)
|
||||||
series: reference("series").optional(),
|
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published game`)
|
||||||
copyrightedCharacters: copyrightedCharacters,
|
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published game`),
|
||||||
lang,
|
|
||||||
relatedStories: z.array(reference("stories")).default([]),
|
|
||||||
relatedGames: z.array(reference("games")).default([]),
|
|
||||||
posts: z
|
|
||||||
.object({
|
|
||||||
eka: z.string().regex(ekaPostUrlRegex),
|
|
||||||
furaffinity: z.string().regex(furaffinityPostUrlRegex),
|
|
||||||
weasyl: z.string().regex(weasylPostUrlRegex),
|
|
||||||
inkbunny: z.string().regex(inkbunnyPostUrlRegex),
|
|
||||||
sofurry: z.string().regex(sofurryPostUrlRegex),
|
|
||||||
mastodon: mastodonPost,
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
.default({}),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Data collections
|
// Data collections
|
||||||
|
@ -175,12 +273,11 @@ const usersCollection = defineCollection({
|
||||||
schema: ({ image }) =>
|
schema: ({ image }) =>
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
// Required
|
// Required parameters
|
||||||
name: z.string(),
|
name: langRecord.or(z.string()),
|
||||||
links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])),
|
links: websiteLinks.partial(),
|
||||||
// Optional
|
// Optional parameters
|
||||||
preferredLink: website.nullish(),
|
preferredLink: websiteLinks.keyof().nullish(),
|
||||||
lang: langRecord.optional(),
|
|
||||||
avatar: image().optional(),
|
avatar: image().optional(),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -195,7 +292,7 @@ const usersCollection = defineCollection({
|
||||||
const seriesCollection = defineCollection({
|
const seriesCollection = defineCollection({
|
||||||
type: "data",
|
type: "data",
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
// Required
|
// Required parameters
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
|
url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
|
||||||
}),
|
}),
|
||||||
|
@ -204,16 +301,18 @@ const seriesCollection = defineCollection({
|
||||||
const tagCategoriesCollection = defineCollection({
|
const tagCategoriesCollection = defineCollection({
|
||||||
type: "data",
|
type: "data",
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
// Required
|
// Required parameters
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
index: z.number().int(),
|
index: z.number().int(),
|
||||||
tags: z.array(
|
tags: z
|
||||||
z.object({
|
.array(
|
||||||
name: z.union([z.string(), langRecord]),
|
z.object({
|
||||||
description: z.string().optional(),
|
name: langRecord.or(z.string()),
|
||||||
related: z.array(z.string()).optional(),
|
description: z.string().trim().optional(),
|
||||||
}),
|
related: z.array(z.string()).default([]),
|
||||||
),
|
}),
|
||||||
|
)
|
||||||
|
.refine((tags) => tags.length, `"tags" cannot be empty`),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ posts:
|
||||||
inkbunny: https://inkbunny.net/s/3262911
|
inkbunny: https://inkbunny.net/s/3262911
|
||||||
sofurry: https://www.sofurry.com/view/2109688
|
sofurry: https://www.sofurry.com/view/2109688
|
||||||
weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game
|
weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game
|
||||||
|
bluesky: https://bsky.app/profile/badmanners.xyz/post/3kmigrf5q2x24
|
||||||
mastodon: https://meow.social/@BadManners/112009918919441027
|
mastodon: https://meow.social/@BadManners/112009918919441027
|
||||||
tags:
|
tags:
|
||||||
- oral vore
|
- oral vore
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
slug: playing-it-safe
|
slug: playing-it-safe
|
||||||
title: Playing It Safe
|
title: Playing It Safe
|
||||||
pubDate: 2024-08-08
|
pubDate: 2024-08-08
|
||||||
isDraft: true
|
|
||||||
authors: bad-manners
|
authors: bad-manners
|
||||||
wordCount: 9900
|
wordCount: 9900
|
||||||
contentWarning: >
|
contentWarning: >
|
||||||
|
|
|
@ -16,6 +16,7 @@ posts:
|
||||||
inkbunny: https://inkbunny.net/s/3283508
|
inkbunny: https://inkbunny.net/s/3283508
|
||||||
sofurry: https://www.sofurry.com/view/2118138
|
sofurry: https://www.sofurry.com/view/2118138
|
||||||
weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident
|
weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident
|
||||||
|
bluesky: https://bsky.app/profile/badmanners.xyz/post/3kok52wijz32c
|
||||||
mastodon: https://meow.social/@BadManners/112157812554023271
|
mastodon: https://meow.social/@BadManners/112157812554023271
|
||||||
tags:
|
tags:
|
||||||
- anthro predator
|
- anthro predator
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: Types of vore
|
name: Types of vore
|
||||||
index: 1
|
index: 1
|
||||||
tags:
|
tags:
|
||||||
- name: { eng: oral vore, tok: moku musi kepeken uta }
|
- name: { en: oral vore, tok: moku musi kepeken uta }
|
||||||
description: Scenarios where prey are consumed by the predator through their mouth.
|
description: Scenarios where prey are consumed by the predator through their mouth.
|
||||||
- name: anal vore
|
- name: anal vore
|
||||||
description: Scenarios where prey are consumed by the predator through their butt/anus.
|
description: Scenarios where prey are consumed by the predator through their butt/anus.
|
||||||
|
|
|
@ -7,7 +7,7 @@ tags:
|
||||||
description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
|
description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
|
||||||
- name: taur predator
|
- name: taur predator
|
||||||
description: Scenarios where at least one of the predators is a multi-legged centaur-like creature, with an animal lower body and anthropomorphic upper body.
|
description: Scenarios where at least one of the predators is a multi-legged centaur-like creature, with an animal lower body and anthropomorphic upper body.
|
||||||
- name: { eng: ambiguous predator, tok: sijelo pi jan pi wawa mute li ale }
|
- name: { en: ambiguous predator, tok: sijelo pi jan pi wawa mute li ale }
|
||||||
description: Scenarios where the body type of at least one of the predators is left ambiguous.
|
description: Scenarios where the body type of at least one of the predators is left ambiguous.
|
||||||
- name: human prey
|
- name: human prey
|
||||||
description: Scenarios where at least one of the prey is a human person.
|
description: Scenarios where at least one of the prey is a human person.
|
||||||
|
@ -15,5 +15,5 @@ tags:
|
||||||
description: Scenarios where at least one of the prey is an anthropomorphic animal, i.e. generally regarded as a "furry".
|
description: Scenarios where at least one of the prey is an anthropomorphic animal, i.e. generally regarded as a "furry".
|
||||||
- name: feral prey
|
- name: feral prey
|
||||||
description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
|
description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
|
||||||
- name: { eng: ambiguous prey, tok: sijelo pi jan pi wawa lili li ale }
|
- name: { en: ambiguous prey, tok: sijelo pi jan pi wawa lili li ale }
|
||||||
description: Scenarios where the body type of at least one of the predators is left ambiguous.
|
description: Scenarios where the body type of at least one of the predators is left ambiguous.
|
||||||
|
|
|
@ -13,7 +13,7 @@ tags:
|
||||||
description: Scenarios where at least one of the predators is a woman and/or female-presenting.
|
description: Scenarios where at least one of the predators is a woman and/or female-presenting.
|
||||||
- name: non-binary predator
|
- name: non-binary predator
|
||||||
description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not.
|
description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not.
|
||||||
- name: { eng: ambiguous gender predator, tok: jan pi wawa mute li meli anu mije }
|
- name: { en: ambiguous gender predator, tok: jan pi wawa mute li meli anu mije }
|
||||||
description: Scenarios where the gender at least one of the predators is left ambiguous.
|
description: Scenarios where the gender at least one of the predators is left ambiguous.
|
||||||
- name: male prey
|
- name: male prey
|
||||||
description: Scenarios where at least one of the prey is a man and/or male-presenting.
|
description: Scenarios where at least one of the prey is a man and/or male-presenting.
|
||||||
|
@ -27,5 +27,5 @@ tags:
|
||||||
- female prey
|
- female prey
|
||||||
- name: non-binary prey
|
- name: non-binary prey
|
||||||
description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not.
|
description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not.
|
||||||
- name: { eng: ambiguous gender prey, tok: jan pi wawa lili li meli anu mije }
|
- name: { en: ambiguous gender prey, tok: jan pi wawa lili li meli anu mije }
|
||||||
description: Scenarios where the gender at least one of the predators is left ambiguous.
|
description: Scenarios where the gender at least one of the predators is left ambiguous.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: Willingness
|
name: Willingness
|
||||||
index: 5
|
index: 5
|
||||||
tags:
|
tags:
|
||||||
- name: { eng: willing predator, tok: jan pi wawa mute li wile e moku musi }
|
- name: { en: willing predator, tok: jan pi wawa mute li wile e moku musi }
|
||||||
description: Scenarios where at least one of the predators participates in vore willingly.
|
description: Scenarios where at least one of the predators participates in vore willingly.
|
||||||
- name: semi-willing predator
|
- name: semi-willing predator
|
||||||
description: Scenarios where the willingness of at least one of the predators might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous.
|
description: Scenarios where the willingness of at least one of the predators might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous.
|
||||||
|
@ -13,7 +13,7 @@ tags:
|
||||||
description: Scenarios where at least one of the prey participates in vore willingly.
|
description: Scenarios where at least one of the prey participates in vore willingly.
|
||||||
- name: semi-willing prey
|
- name: semi-willing prey
|
||||||
description: Scenarios where the willingness of at least one of the prey might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous.
|
description: Scenarios where the willingness of at least one of the prey might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous.
|
||||||
- name: { eng: unwilling prey, tok: jan pi wawa lili li wile ala e moku musi }
|
- name: { en: unwilling prey, tok: jan pi wawa lili li wile ala e moku musi }
|
||||||
description: Scenarios where at least one of the prey participates in vore unwillingly.
|
description: Scenarios where at least one of the prey participates in vore unwillingly.
|
||||||
- name: asleep prey
|
- name: asleep prey
|
||||||
description: Scenarios where at least one of the predators participates in vore while asleep.
|
description: Scenarios where at least one of the predators participates in vore while asleep.
|
||||||
|
|
|
@ -66,4 +66,4 @@ tags:
|
||||||
- name: soul vore
|
- name: soul vore
|
||||||
description: Scenarios where predators consume a soul instead of their prey's body.
|
description: Scenarios where predators consume a soul instead of their prey's body.
|
||||||
- name: Vore Day
|
- name: Vore Day
|
||||||
description: Stories created in commemoration of Vore Day, which is celebrated on August 8ᵗʰ, and/or are set in said day.
|
description: Stories created in commemoration of Vore Day, which is celebrated on August 8th, and/or are set in said day.
|
||||||
|
|
|
@ -5,7 +5,7 @@ tags:
|
||||||
description: Stories made by someone else's request, as a gift.
|
description: Stories made by someone else's request, as a gift.
|
||||||
- name: commission
|
- name: commission
|
||||||
description: Stories made as part of a commission to someone else.
|
description: Stories made as part of a commission to someone else.
|
||||||
- name: { eng: flash fiction, tok: lipu lili }
|
- name: { en: flash fiction, tok: lipu lili }
|
||||||
description: Short-format stories of no more than 2,500 words.
|
description: Short-format stories of no more than 2,500 words.
|
||||||
- name: toki pona
|
- name: toki pona
|
||||||
description: Stories written in toki pona, the language of good.
|
description: Stories written in toki pona, the language of good.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
name: Anonymous
|
name:
|
||||||
lang:
|
en: anonymous
|
||||||
eng: anonymous
|
|
||||||
tok: jan pi nimi ala
|
tok: jan pi nimi ala
|
||||||
links: {}
|
links: {}
|
||||||
preferredLink: ~
|
preferredLink: ~
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: Asof Yeun
|
name: Asof Yeun
|
||||||
links:
|
links:
|
||||||
eka: https://aryion.com/g4/user/asofyeun
|
eka: https://aryion.com/g4/user/asofyeun
|
||||||
furaffinity: https://www.furaffinity.net/user/asofyeun
|
furaffinity: https://www.furaffinity.net/user/AsofYeun
|
||||||
inkbunny: https://inkbunny.net/asofyeun
|
inkbunny: https://inkbunny.net/asofyeun
|
||||||
sofurry: https://asofyeun.sofurry.com/
|
sofurry: https://asofyeun.sofurry.com/
|
||||||
weasyl: https://www.weasyl.com/~asofyeun
|
weasyl: https://www.weasyl.com/~asofyeun
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
name: Bad Manners
|
name:
|
||||||
lang:
|
en: Bad Manners
|
||||||
eng: Bad Manners
|
|
||||||
tok: nasin ike Pemene
|
tok: nasin ike Pemene
|
||||||
avatar: /src/assets/images/logo_bm.png
|
avatar: /src/assets/images/logo_bm.png
|
||||||
links:
|
links:
|
||||||
|
@ -9,8 +8,8 @@ links:
|
||||||
furaffinity: https://www.furaffinity.net/user/BadManners
|
furaffinity: https://www.furaffinity.net/user/BadManners
|
||||||
inkbunny: https://inkbunny.net/BadManners
|
inkbunny: https://inkbunny.net/BadManners
|
||||||
sofurry:
|
sofurry:
|
||||||
- https://bad-manners.sofurry.com/
|
link: https://bad-manners.sofurry.com/
|
||||||
- Bad Manners
|
username: Bad Manners
|
||||||
weasyl: https://www.weasyl.com/~BadManners
|
weasyl: https://www.weasyl.com/~BadManners
|
||||||
twitter: https://twitter.com/BadManners__
|
twitter: https://twitter.com/BadManners__
|
||||||
mastodon: https://meow.social/@BadManners
|
mastodon: https://meow.social/@BadManners
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: Dr. Hans Woofington
|
name: Dr. Hans Woofington
|
||||||
links:
|
links:
|
||||||
furaffinity:
|
furaffinity:
|
||||||
- https://www.furaffinity.net/user/hanslewdington/
|
link: https://www.furaffinity.net/user/hanslewdington/
|
||||||
- Hans_Lewdington
|
username: Hans_Lewdington
|
||||||
preferredLink: furaffinity
|
preferredLink: furaffinity
|
||||||
|
|
|
@ -2,6 +2,6 @@ name: YolkMonkey
|
||||||
links:
|
links:
|
||||||
furaffinity: https://furaffinity.net/user/Vampire101
|
furaffinity: https://furaffinity.net/user/Vampire101
|
||||||
sofurry:
|
sofurry:
|
||||||
- https://vampire101.sofurry.com/
|
link: https://vampire101.sofurry.com/
|
||||||
- Vampire101
|
username: Vampire101
|
||||||
preferredLink: furaffinity
|
preferredLink: furaffinity
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { type GamePlatform, type Lang } from "../content/config";
|
import type { GamePlatform, Lang } from "../content/config";
|
||||||
import { DEFAULT_LANG } from "../content/config";
|
import { DEFAULT_LANG } from "../content/config";
|
||||||
export { DEFAULT_LANG } from "../content/config";
|
export { DEFAULT_LANG } from "../content/config";
|
||||||
|
|
||||||
const UI_STRINGS = {
|
const UI_STRINGS = {
|
||||||
|
// Utility functions
|
||||||
"util/join_names": {
|
"util/join_names": {
|
||||||
eng: (names: string[]) =>
|
en: (names: string[]) =>
|
||||||
names.length <= 1
|
names.length <= 1
|
||||||
? names.join("")
|
? names.join("")
|
||||||
: names.length == 2
|
: names.length == 2
|
||||||
|
@ -13,10 +14,10 @@ const UI_STRINGS = {
|
||||||
tok: (names: string[]) => names.join(" en "),
|
tok: (names: string[]) => names.join(" en "),
|
||||||
},
|
},
|
||||||
"util/capitalize": {
|
"util/capitalize": {
|
||||||
eng: (text: string) => (text.length > 0 ? `${text[0].toUpperCase()}${text.slice(1)}` : ""),
|
en: (text: string) => (text.length > 0 ? `${text[0].toUpperCase()}${text.slice(1)}` : ""),
|
||||||
},
|
},
|
||||||
"util/enumerate": {
|
"util/enumerate": {
|
||||||
eng: (count: number, nounSingular: string, nounPlural: string | undefined) => {
|
en: (count: number, nounSingular: string, nounPlural?: string) => {
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
return `no ${nounPlural ?? nounSingular}`;
|
return `no ${nounPlural ?? nounSingular}`;
|
||||||
}
|
}
|
||||||
|
@ -25,146 +26,205 @@ const UI_STRINGS = {
|
||||||
}
|
}
|
||||||
return `${count} ${nounPlural ?? nounSingular}`;
|
return `${count} ${nounPlural ?? nounSingular}`;
|
||||||
},
|
},
|
||||||
tok: (count: number, nounSingular: string, nounPlural: string | undefined) =>
|
tok: (count: number, nounSingular: string, nounPlural?: string) =>
|
||||||
`${(count > 1 && nounPlural) || nounSingular} ${["ala", "wan", "tu"][count] || "mute"}`,
|
`${(count > 1 && nounPlural) || nounSingular} ${["ala", "wan", "tu"][count] || "mute"}`,
|
||||||
},
|
},
|
||||||
"export_story/writing": {
|
// export-story API functions
|
||||||
eng: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
|
"export_story/authors": {
|
||||||
|
en: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
|
||||||
tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
|
tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
|
||||||
},
|
},
|
||||||
"export_story/request_for": {
|
"export_story/request_for": {
|
||||||
eng: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
|
en: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
|
||||||
},
|
},
|
||||||
"export_story/commissioned_by": {
|
"export_story/commissioned_by": {
|
||||||
eng: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
|
en: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
|
||||||
},
|
},
|
||||||
"story/return_to_stories": {
|
// Shared strings for published content (stories + games)
|
||||||
eng: "Return to stories",
|
"published_content/return_to_series": {
|
||||||
tok: "o tawa e lipu ale",
|
en: (seriesName: string) => `Return to ${seriesName}`,
|
||||||
},
|
},
|
||||||
"story/return_to_series": {
|
"published_content/go_to_description": {
|
||||||
eng: (seriesName: string) => `Return to ${seriesName}`,
|
en: "Go to description",
|
||||||
},
|
|
||||||
"story/go_to_description": {
|
|
||||||
eng: "Go to description",
|
|
||||||
tok: "o tawa e toki lipu",
|
tok: "o tawa e toki lipu",
|
||||||
},
|
},
|
||||||
"story/toggle_dark_mode": {
|
"published_content/toggle_dark_mode": {
|
||||||
eng: "Toggle dark mode",
|
en: "Toggle dark mode",
|
||||||
tok: "o ante e kule lipu",
|
tok: "o ante e kule lipu",
|
||||||
},
|
},
|
||||||
|
"published_content/cover_art_alt": {
|
||||||
|
en: (title: string) => `Cover art for ${title}`,
|
||||||
|
tok: (_title: string) => `sitelen tawa lipu ni`,
|
||||||
|
},
|
||||||
|
"published_content/publish_date": {
|
||||||
|
en: (date: Date) => date.toISOString().slice(0, 10),
|
||||||
|
tok: (date: Date) => `tenpo suno ${date.toISOString().slice(0, 10)}`,
|
||||||
|
},
|
||||||
|
"published_content/publish_date_aria_label": {
|
||||||
|
en: "Publish date",
|
||||||
|
tok: "tenpo pi pana lipu",
|
||||||
|
},
|
||||||
|
"published_content/publish_date_aria_description": {
|
||||||
|
en: (date: Date) =>
|
||||||
|
date.toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
}),
|
||||||
|
tok: (_date: Date) => "",
|
||||||
|
},
|
||||||
|
"published_content/description": {
|
||||||
|
en: "Description",
|
||||||
|
tok: "toki lipu",
|
||||||
|
},
|
||||||
|
"published_content/to_top": {
|
||||||
|
en: "To top",
|
||||||
|
tok: "tawa sewi",
|
||||||
|
},
|
||||||
|
"published_content/tags": {
|
||||||
|
en: "Tags",
|
||||||
|
tok: "nimi kulupu",
|
||||||
|
},
|
||||||
|
"published_content/copyright_year": {
|
||||||
|
en: (year: string | number) => `© ${year}`,
|
||||||
|
tok: (year: string | number) => `© tenpo pi sike suno ${year}`,
|
||||||
|
},
|
||||||
|
"published_content/licenses": {
|
||||||
|
en: "Licenses",
|
||||||
|
tok: "lipu lawa",
|
||||||
|
},
|
||||||
|
"published_content/draft_warning": {
|
||||||
|
en: "DRAFT VERSION – DO NOT REDISTRIBUTE",
|
||||||
|
},
|
||||||
|
"published_content/related_stories": {
|
||||||
|
en: "Related stories",
|
||||||
|
},
|
||||||
|
"published_content/related_games": {
|
||||||
|
en: "Related games",
|
||||||
|
},
|
||||||
|
// Story page-specific strings
|
||||||
|
"story/return_to_stories": {
|
||||||
|
en: "Return to stories",
|
||||||
|
tok: "o tawa e lipu ale",
|
||||||
|
},
|
||||||
|
"story/title_aria_label": {
|
||||||
|
en: "Story title",
|
||||||
|
tok: "nimi lipu",
|
||||||
|
},
|
||||||
|
"story/authors_aria_label": {
|
||||||
|
en: (authors: any[]) => (authors.length == 1 ? "Author" : "Authors"),
|
||||||
|
tok: (_authors: any[]) => "jan pi pali lipu",
|
||||||
|
},
|
||||||
|
"story/requesters_aria_label": {
|
||||||
|
en: (requesters: any[]) => (requesters.length == 1 ? "Requester" : "Requesters"),
|
||||||
|
},
|
||||||
|
"story/commissioners_aria_label": {
|
||||||
|
en: (commissioners: any[]) => (commissioners.length == 1 ? "Commissioner" : "Commissioners"),
|
||||||
|
},
|
||||||
"story/warnings": {
|
"story/warnings": {
|
||||||
eng: (wordCount: number | string | undefined, contentWarning: string) =>
|
en: (wordCount: number | string | undefined, contentWarning: string) =>
|
||||||
wordCount ? `Word count: ${wordCount}. ${contentWarning}` : contentWarning,
|
wordCount ? `Word count: ${wordCount}. ${contentWarning}` : contentWarning,
|
||||||
tok: (_wordCount: number | string | undefined, contentWarning: string) => contentWarning,
|
tok: (_wordCount: number | string | undefined, contentWarning: string) => contentWarning,
|
||||||
},
|
},
|
||||||
"story/publish_date": {
|
"story/article_aria_label": {
|
||||||
eng: (date: string) => date,
|
en: "Story",
|
||||||
tok: (date: string) => `tenpo suno ${date}`,
|
tok: "lipu",
|
||||||
},
|
|
||||||
"story/description": {
|
|
||||||
eng: "Description",
|
|
||||||
tok: "toki lipu",
|
|
||||||
},
|
},
|
||||||
"story/summary": {
|
"story/summary": {
|
||||||
eng: "Summary",
|
en: "Summary",
|
||||||
tok: "lipu tawa tenpo lili",
|
tok: "lipu tawa tenpo lili",
|
||||||
},
|
},
|
||||||
"story/reveal_summary": {
|
"story/reveal_summary": {
|
||||||
eng: "Click to reveal",
|
en: "Click to reveal",
|
||||||
tok: "Click to reveal summary in English",
|
tok: "Click to reveal summary in English",
|
||||||
},
|
},
|
||||||
"story/to_top": {
|
|
||||||
eng: "To top",
|
|
||||||
tok: "tawa sewi",
|
|
||||||
},
|
|
||||||
"story/tags": {
|
|
||||||
eng: "Tags",
|
|
||||||
tok: "nimi kulupu",
|
|
||||||
},
|
|
||||||
"story/copyright_year": {
|
|
||||||
eng: (year: string | number) => `© ${year}`,
|
|
||||||
tok: (year: string | number) => `© tenpo pi sike suno ${year}`,
|
|
||||||
},
|
|
||||||
"story/licenses": {
|
|
||||||
eng: "Licenses",
|
|
||||||
tok: "lipu lawa",
|
|
||||||
},
|
|
||||||
"story/authors": {
|
"story/authors": {
|
||||||
eng: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].eng(authorsList)}`,
|
en: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].en(authorsList)}`,
|
||||||
tok: (authorsList: string[]) =>
|
tok: (authorsList: string[]) =>
|
||||||
authorsList.length > 1
|
authorsList.length > 1
|
||||||
? `lipu ni li tan jan ni: ${UI_STRINGS["util/join_names"].tok(authorsList)}`
|
? `lipu ni li tan jan ni: ${UI_STRINGS["util/join_names"].tok(authorsList)}`
|
||||||
: `lipu ni li tan ${authorsList[0]}`,
|
: `lipu ni li tan ${authorsList[0]}`,
|
||||||
},
|
},
|
||||||
"story/commissioned_by": {
|
"story/commissioned_by": {
|
||||||
eng: (commissionersList: string[]) => `Commissioned by ${UI_STRINGS["util/join_names"].eng(commissionersList)}`,
|
en: (commissionersList: string[]) => `Commissioned by ${UI_STRINGS["util/join_names"].en(commissionersList)}`,
|
||||||
},
|
},
|
||||||
"story/requested_by": {
|
"story/requested_by": {
|
||||||
eng: (requestersList: string[]) => `Requested by ${UI_STRINGS["util/join_names"].eng(requestersList)}`,
|
en: (requestersList: string[]) => `Requested by ${UI_STRINGS["util/join_names"].en(requestersList)}`,
|
||||||
},
|
},
|
||||||
"story/draft_warning": {
|
// Game page-specific strings
|
||||||
eng: "DRAFT VERSION – DO NOT REDISTRIBUTE",
|
"game/return_to_games": {
|
||||||
|
en: "Return to games",
|
||||||
},
|
},
|
||||||
"characters/characters_are_copyrighted_by": {
|
"game/title_aria_label": {
|
||||||
eng: (owner: string, charactersList: string[]) =>
|
en: "Game title",
|
||||||
charactersList.length == 1
|
|
||||||
? `${charactersList[0]} is © ${owner}`
|
|
||||||
: `${UI_STRINGS["util/join_names"].eng(charactersList)} are © ${owner}`,
|
|
||||||
},
|
|
||||||
"characters/all_characters_are_copyrighted_by": {
|
|
||||||
eng: (owner: string) => `All characters are © ${owner}`,
|
|
||||||
},
|
},
|
||||||
"game/platforms": {
|
"game/platforms": {
|
||||||
eng: (platforms: GamePlatform[]) => {
|
en: (platforms: GamePlatform[]) => {
|
||||||
if (platforms.length == 0) {
|
if (platforms.length == 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const translatedPlatforms = platforms.map((platform) => {
|
const translatedPlatforms = platforms.map((platform) => {
|
||||||
const platformLang = UI_STRINGS[`game/platform_${platform}`].eng;
|
const platformLang = UI_STRINGS[`game/platform_${platform}`].en;
|
||||||
if (!platformLang) {
|
if (!platformLang) {
|
||||||
throw new Error(`Invalid platform "${platform}"`);
|
throw new Error(`Invalid platform "${platform}"`);
|
||||||
}
|
}
|
||||||
return platformLang;
|
return platformLang;
|
||||||
});
|
});
|
||||||
return `A game for ${UI_STRINGS["util/join_names"].eng(translatedPlatforms)}.`;
|
return `A game for ${UI_STRINGS["util/join_names"].en(translatedPlatforms)}.`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"game/platform_web": {
|
"game/platform_web": {
|
||||||
eng: "web browsers",
|
en: "web browsers",
|
||||||
},
|
},
|
||||||
"game/platform_windows": {
|
"game/platform_windows": {
|
||||||
eng: "Windows",
|
en: "Windows",
|
||||||
},
|
},
|
||||||
"game/platform_linux": {
|
"game/platform_linux": {
|
||||||
eng: "Linux",
|
en: "Linux",
|
||||||
},
|
},
|
||||||
"game/platform_macos": {
|
"game/platform_macos": {
|
||||||
eng: "macOS",
|
en: "macOS",
|
||||||
},
|
},
|
||||||
"game/platform_android": {
|
"game/platform_android": {
|
||||||
eng: "Android",
|
en: "Android",
|
||||||
},
|
},
|
||||||
"game/platform_ios": {
|
"game/platform_ios": {
|
||||||
eng: "iOS",
|
en: "iOS",
|
||||||
},
|
},
|
||||||
"game/warnings": {
|
"game/warnings": {
|
||||||
eng: (platforms: GamePlatform[], contentWarning: string) =>
|
en: (platforms: GamePlatform[], contentWarning: string) =>
|
||||||
platforms.length > 0 ? `${UI_STRINGS["game/platforms"].eng(platforms)} ${contentWarning}` : contentWarning,
|
platforms.length > 0 ? `${UI_STRINGS["game/platforms"].en(platforms)} ${contentWarning}` : contentWarning,
|
||||||
},
|
},
|
||||||
|
"game/article_aria_label": {
|
||||||
|
en: "Game",
|
||||||
|
},
|
||||||
|
// Copyrighted character-related strings
|
||||||
|
"characters/copyrighted_characters_aria_label": {
|
||||||
|
en: "Copyrighted characters",
|
||||||
|
},
|
||||||
|
"characters/characters_are_copyrighted_by": {
|
||||||
|
en: (owner: string, charactersList: string[]) =>
|
||||||
|
charactersList.length == 1
|
||||||
|
? `${charactersList[0]} is © ${owner}`
|
||||||
|
: `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
|
||||||
|
},
|
||||||
|
"characters/all_characters_are_copyrighted_by": {
|
||||||
|
en: (owner: string) => `All characters are © ${owner}`,
|
||||||
|
},
|
||||||
|
// Tag-related strings
|
||||||
"tag/total_works_with_tag": {
|
"tag/total_works_with_tag": {
|
||||||
eng: (tag: string, storiesCount: number, gamesCount: number) => {
|
en: (tag: string, storiesCount: number, gamesCount: number) => {
|
||||||
const content = [];
|
const content = [];
|
||||||
if (storiesCount > 0) {
|
if (storiesCount > 0) {
|
||||||
content.push(UI_STRINGS["util/enumerate"].eng(storiesCount, "story", "stories"));
|
content.push(UI_STRINGS["util/enumerate"].en(storiesCount, "story", "stories"));
|
||||||
}
|
}
|
||||||
if (gamesCount > 0) {
|
if (gamesCount > 0) {
|
||||||
content.push(UI_STRINGS["util/enumerate"].eng(gamesCount, "game", "games"));
|
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
|
||||||
}
|
}
|
||||||
if (content.length == 0) {
|
if (content.length == 0) {
|
||||||
return `No works tagged with "${tag}".`;
|
return `No works tagged with "${tag}".`;
|
||||||
}
|
}
|
||||||
return UI_STRINGS["util/capitalize"].eng(`${UI_STRINGS["util/join_names"].eng(content)} tagged with "${tag}".`);
|
return UI_STRINGS["util/capitalize"].en(`${UI_STRINGS["util/join_names"].en(content)} tagged with "${tag}".`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -176,6 +236,9 @@ type TranslationEntry<T> = { [DEFAULT_LANG]: T } & {
|
||||||
type TranslationArgs<K extends TranslationKey> =
|
type TranslationArgs<K extends TranslationKey> =
|
||||||
(typeof UI_STRINGS)[K] extends TranslationEntry<infer T> ? (T extends (...args: infer A) => string ? A : []) : never;
|
(typeof UI_STRINGS)[K] extends TranslationEntry<infer T> ? (T extends (...args: infer A) => string ? A : []) : never;
|
||||||
|
|
||||||
|
/** Translates some text according to the provided language, a translation key,
|
||||||
|
* and optionally any required arguments.
|
||||||
|
*/
|
||||||
export function t<K extends TranslationKey>(lang: Lang, key: K, ...args: TranslationArgs<K>): string {
|
export function t<K extends TranslationKey>(lang: Lang, key: K, ...args: TranslationArgs<K>): string {
|
||||||
if (key in UI_STRINGS) {
|
if (key in UI_STRINGS) {
|
||||||
const translation: string | ((...args: TranslationArgs<K>) => string) =
|
const translation: string | ((...args: TranslationArgs<K>) => string) =
|
||||||
|
|
|
@ -6,12 +6,13 @@ import AgeRestrictedModal from "../components/AgeRestrictedModal.astro";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pageTitle?: string;
|
pageTitle?: string;
|
||||||
|
lang?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { pageTitle } = Astro.props;
|
const { pageTitle = "Gallery", lang = "en" } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang={lang}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
@ -23,7 +24,7 @@ const { pageTitle } = Astro.props;
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{pageTitle || "Gallery"} | Bad Manners</title>
|
<title>{pageTitle} | Bad Manners</title>
|
||||||
<link rel="me" href="https://meow.social/@BadManners" />
|
<link rel="me" href="https://meow.social/@BadManners" />
|
||||||
<link
|
<link
|
||||||
rel="alternate"
|
rel="alternate"
|
||||||
|
|
|
@ -18,8 +18,8 @@ const { props } = Astro;
|
||||||
const series = props.series && (await getEntry(props.series));
|
const series = props.series && (await getEntry(props.series));
|
||||||
const authorsList = await getEntries(props.authors);
|
const authorsList = await getEntries(props.authors);
|
||||||
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
||||||
// const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||||
const categorizedTags = Object.fromEntries(
|
const categorizedTags = Object.fromEntries(
|
||||||
(await getCollection("tag-categories")).flatMap((category) =>
|
(await getCollection("tag-categories")).flatMap((category) =>
|
||||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||||
|
@ -44,10 +44,10 @@ const thumbnail =
|
||||||
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout pageTitle={props.title}>
|
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||||
<meta property="og:description" content={props.contentWarning} />
|
<meta property="og:description" content={t(props.lang, "game/warnings", props.platforms, props.contentWarning)} />
|
||||||
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
<meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" />
|
||||||
{
|
{
|
||||||
thumbnail ? (
|
thumbnail ? (
|
||||||
|
@ -55,7 +55,7 @@ const thumbnail =
|
||||||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||||
<meta
|
<meta
|
||||||
property="og:image:alt"
|
property="og:image:alt"
|
||||||
content={`Cover art for ${props.title}`}
|
content={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||||
data-pagefind-meta="image_alt[content]"
|
data-pagefind-meta="image_alt[content]"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -78,7 +78,9 @@ const thumbnail =
|
||||||
<a
|
<a
|
||||||
href={series ? series.data.url : "/games"}
|
href={series ? series.data.url : "/games"}
|
||||||
class="text-link my-1 h-9 w-9 p-2"
|
class="text-link my-1 h-9 w-9 p-2"
|
||||||
aria-label={`Return to ${series ? series.data.name : "games"}`}
|
aria-label={series
|
||||||
|
? t(props.lang, "published_content/return_to_series", series.data.name)
|
||||||
|
: t(props.lang, "game/return_to_games")}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
|
@ -89,7 +91,7 @@ const thumbnail =
|
||||||
<a
|
<a
|
||||||
href="#description"
|
href="#description"
|
||||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-label="Go to description"
|
aria-label={t(props.lang, "published_content/go_to_description")}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
|
@ -100,7 +102,7 @@ const thumbnail =
|
||||||
<button
|
<button
|
||||||
data-dark-mode
|
data-dark-mode
|
||||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-label="Toggle dark mode"
|
aria-label={t(props.lang, "published_content/toggle_dark_mode")}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
|
@ -120,7 +122,11 @@ const thumbnail =
|
||||||
data-pagefind-body={props.isDraft ? undefined : ""}
|
data-pagefind-body={props.isDraft ? undefined : ""}
|
||||||
data-pagefind-meta="type:game"
|
data-pagefind-meta="type:game"
|
||||||
>
|
>
|
||||||
<h1 id="game-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100">
|
<h1
|
||||||
|
id="game-title"
|
||||||
|
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||||
|
aria-label={t(props.lang, "game/title_aria_label")}
|
||||||
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</h1>
|
</h1>
|
||||||
<section
|
<section
|
||||||
|
@ -136,7 +142,7 @@ const thumbnail =
|
||||||
{
|
{
|
||||||
props.isDraft ? (
|
props.isDraft ? (
|
||||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||||
{t(props.lang, "story/draft_warning")}
|
{t(props.lang, "published_content/draft_warning")}
|
||||||
</p>
|
</p>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
@ -155,7 +161,7 @@ const thumbnail =
|
||||||
<img
|
<img
|
||||||
loading="eager"
|
loading="eager"
|
||||||
src={thumbnail.src}
|
src={thumbnail.src}
|
||||||
alt={`Cover art for ${props.title}`}
|
alt={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||||
width={props.thumbnailWidth}
|
width={props.thumbnailWidth}
|
||||||
height={props.thumbnailHeight}
|
height={props.thumbnailHeight}
|
||||||
class="mx-auto my-5 shadow-lg"
|
class="mx-auto my-5 shadow-lg"
|
||||||
|
@ -165,7 +171,7 @@ const thumbnail =
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||||
<article id="game" class="pr-1 font-serif">
|
<article id="game" class="pr-1 font-serif" aria-label={t(props.lang, "game/article_aria_label")}>
|
||||||
<Prose>
|
<Prose>
|
||||||
<slot />
|
<slot />
|
||||||
</Prose>
|
</Prose>
|
||||||
|
@ -177,28 +183,26 @@ const thumbnail =
|
||||||
id="draft-warning-bottom"
|
id="draft-warning-bottom"
|
||||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||||
>
|
>
|
||||||
{t(props.lang, "story/draft_warning")}
|
{t(props.lang, "published_content/draft_warning")}
|
||||||
</p>
|
</p>
|
||||||
) : props.pubDate ? (
|
) : props.pubDate ? (
|
||||||
<p
|
<p
|
||||||
id="publish-date"
|
id="publish-date"
|
||||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||||
aria-label="Publish date"
|
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||||
aria-description={props.pubDate.toLocaleDateString("en-US", {
|
aria-description={
|
||||||
month: "long",
|
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||||
day: "numeric",
|
}
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
data-pagefind-index-attrs="aria-description"
|
data-pagefind-index-attrs="aria-description"
|
||||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||||
>
|
>
|
||||||
{t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(0, 10))}
|
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||||
</p>
|
</p>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
{t(props.lang, "story/description")}
|
{t(props.lang, "published_content/description")}
|
||||||
</h2>
|
</h2>
|
||||||
<Prose>
|
<Prose>
|
||||||
<Markdown of={props.description} />
|
<Markdown of={props.description} />
|
||||||
|
@ -211,14 +215,50 @@ const thumbnail =
|
||||||
><path
|
><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"
|
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
|
></path></svg
|
||||||
><span>{t(props.lang, "story/to_top")}</span></a
|
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
relatedStories.length > 0 ? (
|
||||||
|
<section id="related" aria-describedby="title-related" class="my-5">
|
||||||
|
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
|
{t(props.lang, "published_content/related_stories")}
|
||||||
|
</h2>
|
||||||
|
<Prose>
|
||||||
|
<ul>
|
||||||
|
{relatedStories.map((story) => (
|
||||||
|
<li>
|
||||||
|
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Prose>
|
||||||
|
</section>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
relatedGames.length > 0 ? (
|
||||||
|
<section id="related" aria-describedby="title-related" class="my-5">
|
||||||
|
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
|
{t(props.lang, "published_content/related_games")}
|
||||||
|
</h2>
|
||||||
|
<Prose>
|
||||||
|
<ul>
|
||||||
|
{relatedGames.map((game) => (
|
||||||
|
<li>
|
||||||
|
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Prose>
|
||||||
|
</section>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
tags.length > 0 ? (
|
tags.length > 0 ? (
|
||||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
Tags
|
{t(props.lang, "published_content/tags")}
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||||
{tags.map(({ id, name }) => (
|
{tags.map(({ id, name }) => (
|
||||||
|
@ -232,16 +272,12 @@ const thumbnail =
|
||||||
</section>
|
</section>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<MastodonComments
|
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||||
instance={props.posts.mastodon?.instance}
|
|
||||||
user={props.posts.mastodon?.user}
|
|
||||||
postId={props.posts.mastodon?.postId}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||||
<span>{t(props.lang, "story/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||||
>{t(props.lang, "story/licenses")}</a
|
>{t(props.lang, "published_content/licenses")}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,21 +17,15 @@ import { formatCopyrightedCharacters } from "../utils/format_copyrighted_charact
|
||||||
type Props = CollectionEntry<"stories">["data"];
|
type Props = CollectionEntry<"stories">["data"];
|
||||||
|
|
||||||
const { props } = Astro;
|
const { props } = Astro;
|
||||||
let prev = props.prev && (await getEntry(props.prev));
|
const prev = props.prev && (await getEntry(props.prev));
|
||||||
if (prev && prev.data.isDraft) {
|
const next = props.next && (await getEntry(props.next));
|
||||||
prev = undefined;
|
|
||||||
}
|
|
||||||
let next = props.next && (await getEntry(props.next));
|
|
||||||
if (next && next.data.isDraft) {
|
|
||||||
next = undefined;
|
|
||||||
}
|
|
||||||
const series = props.series && (await getEntry(props.series));
|
const series = props.series && (await getEntry(props.series));
|
||||||
const authorsList = await getEntries(props.authors);
|
const authorsList = await getEntries(props.authors);
|
||||||
const commissionersList = props.commissioner && (await getEntries(props.commissioner));
|
const commissionersList = props.commissioner && (await getEntries(props.commissioner));
|
||||||
const requestersList = props.requester && (await getEntries(props.requester));
|
const requestersList = props.requester && (await getEntries(props.requester));
|
||||||
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
||||||
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||||
const categorizedTags = Object.fromEntries(
|
const categorizedTags = Object.fromEntries(
|
||||||
(await getCollection("tag-categories")).flatMap((category) =>
|
(await getCollection("tag-categories")).flatMap((category) =>
|
||||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||||
|
@ -57,7 +51,7 @@ const thumbnail =
|
||||||
const wordCount = props.wordCount?.toString();
|
const wordCount = props.wordCount?.toString();
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout pageTitle={props.title}>
|
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
<meta property="og:title" content={props.title} data-pagefind-meta="title[content]" />
|
||||||
<meta property="og:description" content={t(props.lang, "story/warnings", wordCount, props.contentWarning)} />
|
<meta property="og:description" content={t(props.lang, "story/warnings", wordCount, props.contentWarning)} />
|
||||||
|
@ -68,7 +62,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||||
<meta
|
<meta
|
||||||
property="og:image:alt"
|
property="og:image:alt"
|
||||||
content={`Cover art for ${props.shortTitle || props.title}`}
|
content={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
|
||||||
data-pagefind-meta="image_alt[content]"
|
data-pagefind-meta="image_alt[content]"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -92,7 +86,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
href={series ? series.data.url : "/stories/1"}
|
href={series ? series.data.url : "/stories/1"}
|
||||||
class="text-link my-1 h-9 w-9 p-2"
|
class="text-link my-1 h-9 w-9 p-2"
|
||||||
aria-label={series
|
aria-label={series
|
||||||
? t(props.lang, "story/return_to_series", series.data.name)
|
? t(props.lang, "published_content/return_to_series", series.data.name)
|
||||||
: t(props.lang, "story/return_to_stories")}
|
: t(props.lang, "story/return_to_stories")}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||||
|
@ -104,7 +98,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
<a
|
<a
|
||||||
href="#description"
|
href="#description"
|
||||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-label={t(props.lang, "story/go_to_description")}
|
aria-label={t(props.lang, "published_content/go_to_description")}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
|
@ -115,7 +109,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
<button
|
<button
|
||||||
data-dark-mode
|
data-dark-mode
|
||||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||||
aria-label={t(props.lang, "story/toggle_dark_mode")}
|
aria-label={t(props.lang, "published_content/toggle_dark_mode")}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
|
@ -136,7 +130,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
data-pagefind-meta="type:story"
|
data-pagefind-meta="type:story"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
prev || next ? (
|
(prev && !prev.data.isDraft) || (next && !next.data.isDraft) ? (
|
||||||
<div class="print:hidden">
|
<div class="print:hidden">
|
||||||
<div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
<div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||||
{prev ? (
|
{prev ? (
|
||||||
|
@ -170,7 +164,11 @@ const wordCount = props.wordCount?.toString();
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<h1 id="story-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100">
|
<h1
|
||||||
|
id="story-title"
|
||||||
|
class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"
|
||||||
|
aria-label={t(props.lang, "story/title_aria_label")}
|
||||||
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</h1>
|
</h1>
|
||||||
<section
|
<section
|
||||||
|
@ -180,13 +178,6 @@ const wordCount = props.wordCount?.toString();
|
||||||
<Authors lang={props.lang}>
|
<Authors lang={props.lang}>
|
||||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||||
</Authors>
|
</Authors>
|
||||||
{
|
|
||||||
props.isDraft ? (
|
|
||||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
|
||||||
{t(props.lang, "story/draft_warning")}
|
|
||||||
</p>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
requestersList && (
|
requestersList && (
|
||||||
<Requesters lang={props.lang}>
|
<Requesters lang={props.lang}>
|
||||||
|
@ -205,6 +196,13 @@ const wordCount = props.wordCount?.toString();
|
||||||
</Commissioners>
|
</Commissioners>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
props.isDraft ? (
|
||||||
|
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||||
|
{t(props.lang, "published_content/draft_warning")}
|
||||||
|
</p>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
<div id="content-warning">
|
<div id="content-warning">
|
||||||
<p>
|
<p>
|
||||||
{t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
{t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
||||||
|
@ -218,7 +216,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
<img
|
<img
|
||||||
loading="eager"
|
loading="eager"
|
||||||
src={thumbnail.src}
|
src={thumbnail.src}
|
||||||
alt={`Cover art for ${props.shortTitle || props.title}`}
|
alt={t(props.lang, "published_content/cover_art_alt", props.shortTitle || props.title)}
|
||||||
width={props.thumbnailWidth}
|
width={props.thumbnailWidth}
|
||||||
height={props.thumbnailHeight}
|
height={props.thumbnailHeight}
|
||||||
class="mx-auto my-5 shadow-lg"
|
class="mx-auto my-5 shadow-lg"
|
||||||
|
@ -227,7 +225,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
<hr class="mx-auto my-10 w-[80%] max-w-xl border-stone-400 dark:border-stone-600" />
|
||||||
<article id="story" class="pr-1 font-serif">
|
<article id="story" class="pr-1 font-serif" aria-label={t(props.lang, "story/article_aria_label")}>
|
||||||
<Prose>
|
<Prose>
|
||||||
<slot />
|
<slot />
|
||||||
</Prose>
|
</Prose>
|
||||||
|
@ -239,28 +237,26 @@ const wordCount = props.wordCount?.toString();
|
||||||
id="draft-warning-bottom"
|
id="draft-warning-bottom"
|
||||||
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
class="py-2 text-center font-serif text-2xl font-semibold not-italic text-red-600"
|
||||||
>
|
>
|
||||||
{t(props.lang, "story/draft_warning")}
|
{t(props.lang, "published_content/draft_warning")}
|
||||||
</p>
|
</p>
|
||||||
) : props.pubDate ? (
|
) : props.pubDate ? (
|
||||||
<p
|
<p
|
||||||
id="publish-date"
|
id="publish-date"
|
||||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||||
aria-label="Publish date"
|
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||||
aria-description={props.pubDate.toLocaleDateString("en-US", {
|
aria-description={
|
||||||
month: "long",
|
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||||
day: "numeric",
|
}
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
data-pagefind-index-attrs="aria-description"
|
data-pagefind-index-attrs="aria-description"
|
||||||
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
data-pagefind-meta={`date:${props.pubDate.toISOString().slice(0, 10)}`}
|
||||||
>
|
>
|
||||||
{t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(0, 10))}
|
{t(props.lang, "published_content/publish_date", props.pubDate)}
|
||||||
</p>
|
</p>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
{t(props.lang, "story/description")}
|
{t(props.lang, "published_content/description")}
|
||||||
</h2>
|
</h2>
|
||||||
<Prose>
|
<Prose>
|
||||||
<Markdown of={props.description} />
|
<Markdown of={props.description} />
|
||||||
|
@ -292,7 +288,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
><path
|
><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"
|
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
|
></path></svg
|
||||||
><span>{t(props.lang, "story/to_top")}</span></a
|
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
|
@ -335,13 +331,31 @@ const wordCount = props.wordCount?.toString();
|
||||||
relatedStories.length > 0 ? (
|
relatedStories.length > 0 ? (
|
||||||
<section id="related" aria-describedby="title-related" class="my-5">
|
<section id="related" aria-describedby="title-related" class="my-5">
|
||||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
Related stories
|
{t(props.lang, "published_content/related_stories")}
|
||||||
</h2>
|
</h2>
|
||||||
<Prose>
|
<Prose>
|
||||||
<ul>
|
<ul>
|
||||||
{relatedStories.map((stories) => (
|
{relatedStories.map((story) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/stories/${stories.slug}`}>{stories.data.title}</a>
|
<a href={`/stories/${story.slug}`}>{story.data.title}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Prose>
|
||||||
|
</section>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
relatedGames.length > 0 ? (
|
||||||
|
<section id="related" aria-describedby="title-related" class="my-5">
|
||||||
|
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
|
{t(props.lang, "published_content/related_games")}
|
||||||
|
</h2>
|
||||||
|
<Prose>
|
||||||
|
<ul>
|
||||||
|
{relatedGames.map((game) => (
|
||||||
|
<li>
|
||||||
|
<a href={`/games/${game.slug}`}>{game.data.title}</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -353,7 +367,7 @@ const wordCount = props.wordCount?.toString();
|
||||||
tags.length > 0 ? (
|
tags.length > 0 ? (
|
||||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||||
{t(props.lang, "story/tags")}
|
{t(props.lang, "published_content/tags")}
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||||
{tags.map(({ id, name }) => (
|
{tags.map(({ id, name }) => (
|
||||||
|
@ -367,16 +381,12 @@ const wordCount = props.wordCount?.toString();
|
||||||
</section>
|
</section>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<MastodonComments
|
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||||
instance={props.posts.mastodon?.instance}
|
|
||||||
user={props.posts.mastodon?.user}
|
|
||||||
postId={props.posts.mastodon?.postId}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||||
<span>{t(props.lang, "story/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
<span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
|
||||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||||
>{t(props.lang, "story/licenses")}</a
|
>{t(props.lang, "published_content/licenses")}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { APIRoute, GetStaticPaths } from "astro";
|
import type { APIRoute, GetStaticPaths } from "astro";
|
||||||
import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
|
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
|
||||||
import type { Website } from "../../../content/config";
|
import type { Lang, Website } from "../../../content/config";
|
||||||
import { t } from "../../../i18n";
|
import { t } from "../../../i18n";
|
||||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||||
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
||||||
|
@ -24,72 +24,8 @@ type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: in
|
||||||
|
|
||||||
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
||||||
const link = user.data.links[website];
|
const link = user.data.links[website];
|
||||||
if (link) {
|
if (link?.username) {
|
||||||
if (typeof link === "string") {
|
return link.username;
|
||||||
switch (website) {
|
|
||||||
case "website":
|
|
||||||
break;
|
|
||||||
case "eka":
|
|
||||||
const ekaMatch = link.match(/^.*\baryion\.com\/g4\/user\/([^\/]+)\/?$/);
|
|
||||||
if (ekaMatch && ekaMatch[1]) {
|
|
||||||
return ekaMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "furaffinity":
|
|
||||||
const faMatch = link.match(/^.*\bfuraffinity\.net\/user\/([^\/]+)\/?$/);
|
|
||||||
if (faMatch && faMatch[1]) {
|
|
||||||
return faMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "inkbunny":
|
|
||||||
const ibMatch = link.match(/^.*\binkbunny\.net\/([^\/]+)\/?$/);
|
|
||||||
if (ibMatch && ibMatch[1]) {
|
|
||||||
return ibMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "sofurry":
|
|
||||||
const sfMatch = link.match(/^(?:https?:\/\/)?([^\.]+).sofurry.com\b.*$/);
|
|
||||||
if (sfMatch && sfMatch[1]) {
|
|
||||||
return sfMatch[1].replaceAll("-", " ");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "weasyl":
|
|
||||||
const weasylMatch = link.match(/^.*\bweasyl\.com\/\~([^\/]+)\/?$/);
|
|
||||||
if (weasylMatch && weasylMatch[1]) {
|
|
||||||
return weasylMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "twitter":
|
|
||||||
const twitterMatch = link.match(/^.*(?:\btwitter\.com|\bx\.com)\/@?([^\/]+)\/?$/);
|
|
||||||
if (twitterMatch && twitterMatch[1]) {
|
|
||||||
return twitterMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "mastodon":
|
|
||||||
const mastodonMatch = link.match(/^(?:https?\:\/\/)?([^\/]+)\/(?:@|users\/)([^\/]+)\/?$/);
|
|
||||||
if (mastodonMatch && mastodonMatch[1] && mastodonMatch[2]) {
|
|
||||||
return `${mastodonMatch[2]}@${mastodonMatch[1]}`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "bluesky":
|
|
||||||
const bskyMatch = link.match(/^.*\bbsky\.app\/profile\/([^\/]+)\/?$/);
|
|
||||||
if (bskyMatch && bskyMatch[1]) {
|
|
||||||
return bskyMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "itaku":
|
|
||||||
const itakuMatch = link.match(/^.*\bitaku\.ee\/profile\/([^\/]+)\/?$/);
|
|
||||||
if (itakuMatch && itakuMatch[1]) {
|
|
||||||
return itakuMatch[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
let _: never = website;
|
|
||||||
throw new Error(`Unhandled website "${website}"`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return link[1].replace(/^@/, "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
|
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
|
||||||
}
|
}
|
||||||
|
@ -99,20 +35,21 @@ function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): b
|
||||||
return !preferredLink || preferredLink == website;
|
return !preferredLink || preferredLink == website;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName): string {
|
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName, lang: Lang): string {
|
||||||
|
const { links, preferredLink } = user.data;
|
||||||
switch (website) {
|
switch (website) {
|
||||||
case "eka":
|
case "eka":
|
||||||
if ("eka" in user.data.links) {
|
if ("eka" in links) {
|
||||||
return `:icon${getUsernameForWebsite(user, "eka")}:`;
|
return `:icon${getUsernameForWebsite(user, "eka")}:`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "furaffinity":
|
case "furaffinity":
|
||||||
if ("furaffinity" in user.data.links) {
|
if ("furaffinity" in links) {
|
||||||
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
|
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "weasyl":
|
case "weasyl":
|
||||||
if ("weasyl" in user.data.links) {
|
if ("weasyl" in links) {
|
||||||
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
||||||
|
@ -123,7 +60,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "inkbunny":
|
case "inkbunny":
|
||||||
if ("inkbunny" in user.data.links) {
|
if ("inkbunny" in links) {
|
||||||
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
|
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
|
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
|
||||||
|
@ -134,7 +71,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "sofurry":
|
case "sofurry":
|
||||||
if ("sofurry" in user.data.links) {
|
if ("sofurry" in links) {
|
||||||
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
|
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
|
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
|
||||||
|
@ -143,11 +80,12 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled ExportWebsite "${website}"`);
|
const unknown: never = website;
|
||||||
|
throw new Error(`Unhandled export website "${unknown}"`);
|
||||||
}
|
}
|
||||||
if (user.data.preferredLink) {
|
if (preferredLink) {
|
||||||
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
|
const preferred = links[preferredLink]!;
|
||||||
return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
|
return `[${getUsernameForLang(user, lang)}](${preferred.link})`;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
|
`No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
|
||||||
|
@ -182,14 +120,14 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
const description = Object.fromEntries(
|
const description = Object.fromEntries(
|
||||||
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
||||||
const u = (user: CollectionEntry<"users">) =>
|
const u = (user: CollectionEntry<"users">) =>
|
||||||
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website);
|
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website, lang);
|
||||||
const storyDescription = (
|
const storyDescription = (
|
||||||
[
|
[
|
||||||
story.data.description,
|
story.data.description,
|
||||||
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
||||||
t(
|
t(
|
||||||
lang,
|
lang,
|
||||||
"export_story/writing",
|
"export_story/authors",
|
||||||
authorsList.map((author) => u(author)),
|
authorsList.map((author) => u(author)),
|
||||||
),
|
),
|
||||||
requestersList &&
|
requestersList &&
|
||||||
|
@ -216,12 +154,14 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
||||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||||
);
|
);
|
||||||
if (exportFormat === "bbcode") {
|
switch (exportFormat) {
|
||||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
case "bbcode":
|
||||||
} else if (exportFormat === "markdown") {
|
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
||||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
case "markdown":
|
||||||
} else {
|
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||||
throw new Error(`Unhandled export format "${exportFormat}"`);
|
default:
|
||||||
|
const unknown: never = exportFormat;
|
||||||
|
throw new Error(`Unknown export format "${unknown}"`);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -252,13 +192,12 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
story: storyText,
|
story: storyText,
|
||||||
description,
|
description,
|
||||||
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
||||||
}),
|
}),
|
||||||
{ headers },
|
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
const content = { isAlive: true };
|
|
||||||
|
|
||||||
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
|
||||||
|
|
||||||
export const GET: APIRoute = () => {
|
export const GET: APIRoute = () => {
|
||||||
if (import.meta.env.PROD) {
|
if (import.meta.env.PROD) {
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
}
|
}
|
||||||
return new Response(JSON.stringify(content), { headers });
|
return new Response(JSON.stringify({ isAlive: true }), {
|
||||||
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,8 +24,7 @@ function toNoonUTCDate(date: Date) {
|
||||||
const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
|
const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
|
||||||
const userName = getUsernameForLang(user, lang);
|
const userName = getUsernameForLang(user, lang);
|
||||||
if (user.data.preferredLink) {
|
if (user.data.preferredLink) {
|
||||||
const link = user.data.links[user.data.preferredLink]!;
|
return `<a href="${user.data.links[user.data.preferredLink]!.link}">${userName}</a>`;
|
||||||
return `<a href="${typeof link === "string" ? link : link[0]}">${userName}</a>`;
|
|
||||||
}
|
}
|
||||||
return userName;
|
return userName;
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,17 +18,6 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const game = Astro.props;
|
const game = Astro.props;
|
||||||
if (!game.data.isDraft) {
|
|
||||||
if (!game.data.pubDate) {
|
|
||||||
throw new Error(`Missing "pubDate" for published game ${game.data.title} ("${game.slug}")`);
|
|
||||||
}
|
|
||||||
if (!game.data.thumbnail) {
|
|
||||||
throw new Error(`Missing "thumbnail" for published game ${game.data.title} ("${game.slug}")`);
|
|
||||||
}
|
|
||||||
if (game.data.tags.length == 0) {
|
|
||||||
throw new Error(`Missing "tags" for published game ${game.data.title} ("${game.slug}")`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { Content } = await game.render();
|
const { Content } = await game.render();
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
|
import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
|
@ -20,21 +20,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
|
|
||||||
const story = Astro.props;
|
const story = Astro.props;
|
||||||
const readingTime = getReadingTime(story.body);
|
const readingTime = getReadingTime(story.body);
|
||||||
if (!story.data.isDraft) {
|
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 150) {
|
||||||
if (!story.data.wordCount) {
|
|
||||||
throw new Error(`Missing "wordCount" for published story ${story.data.title} ("${story.slug}")`);
|
|
||||||
}
|
|
||||||
if (!story.data.pubDate) {
|
|
||||||
throw new Error(`Missing "pubDate" for published story ${story.data.title} ("${story.slug}")`);
|
|
||||||
}
|
|
||||||
if (!story.data.thumbnail) {
|
|
||||||
throw new Error(`Missing "thumbnail" for published story ${story.data.title} ("${story.slug}")`);
|
|
||||||
}
|
|
||||||
if (story.data.tags.length == 0) {
|
|
||||||
throw new Error(`Missing "tags" for published story ${story.data.title} ("${story.slug}")`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 135) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
|
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
|
||||||
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
||||||
|
|
|
@ -10,65 +10,53 @@ interface Tag {
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [stories, games, tagCategories] = await Promise.all([
|
const [stories, games, tagCategories, seriesCollection] = await Promise.all([
|
||||||
getCollection("stories"),
|
getCollection("stories"),
|
||||||
getCollection("games"),
|
getCollection("games"),
|
||||||
getCollection("tag-categories"),
|
getCollection("tag-categories"),
|
||||||
|
getCollection("series"),
|
||||||
]);
|
]);
|
||||||
const tagsSet = new Set<string>();
|
const uncategorizedTagsSet = new Set<string>();
|
||||||
const draftOnlyTagsSet = new Set<string>();
|
const draftOnlyTagsSet = new Set<string>();
|
||||||
const seriesCollection = await getCollection("series");
|
[stories, games].flat().forEach(({ data: { isDraft, tags } }) => {
|
||||||
// Add tags from non-drafts to set; then, add tags only from drafts to separate set
|
if (isDraft) {
|
||||||
[stories, games]
|
tags.forEach((tag) => draftOnlyTagsSet.add(tag));
|
||||||
.flat()
|
} else {
|
||||||
.sort((a, b) => (a.data.isDraft ? 1 : b.data.isDraft ? -1 : 0))
|
tags.forEach((tag) => uncategorizedTagsSet.add(tag));
|
||||||
.forEach((value) => {
|
}
|
||||||
if (value.data.isDraft) {
|
});
|
||||||
value.data.tags.forEach((tag) => {
|
// Tags from published content shouldn't be included in drafts-only set
|
||||||
if (!tagsSet.has(tag)) {
|
uncategorizedTagsSet.forEach((tag) => draftOnlyTagsSet.delete(tag));
|
||||||
draftOnlyTagsSet.add(tag);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
value.data.tags.forEach((tag) => {
|
|
||||||
tagsSet.add(tag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const uncategorizedTagsSet = new Set(tagsSet);
|
const uniqueSlugs = new Set<string>();
|
||||||
const categorizedTags = tagCategories
|
const categorizedTags = tagCategories
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.data.index == b.data.index) {
|
if (a.data.index == b.data.index) {
|
||||||
throw new Error(
|
throw new Error(`Found tag categories with same index value ${a.data.index} ("${a.id}", "${b.id}")`);
|
||||||
`Found tag categories with same index value ${a.data.index} ("${a.data.name}", "${b.data.name}")`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return a.data.index - b.data.index;
|
return a.data.index - b.data.index;
|
||||||
})
|
})
|
||||||
.map((category) => {
|
.map((category) => {
|
||||||
const tagList = category.data.tags.map<Tag>(({ name, description }) => {
|
const tagList = category.data.tags.map<Tag>(({ name, description }) => {
|
||||||
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ");
|
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ");
|
||||||
const tag = typeof name === "string" ? name : name["eng"];
|
const tag = typeof name === "string" ? name : name.en;
|
||||||
const id = slug(tag);
|
return { id: slug(tag), name: tag, description };
|
||||||
return { id, name: tag, description };
|
|
||||||
});
|
});
|
||||||
tagList.forEach(({ name }, index) => {
|
tagList.forEach(({ id, name }) => {
|
||||||
if (index !== tagList.findLastIndex(({ name: otherTag }) => name == otherTag)) {
|
if (uniqueSlugs.has(id)) {
|
||||||
throw new Error(`Duplicated tag "${name}" found in multiple tag-categories lists`);
|
throw new Error(`Duplicated tag "${name}" found in multiple tag-categories entries`);
|
||||||
}
|
}
|
||||||
|
uniqueSlugs.add(id);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
name: category.data.name,
|
name: category.data.name,
|
||||||
id: category.id,
|
id: slug(category.data.name),
|
||||||
tags: tagList.filter(({ name }) => {
|
tags: tagList.filter(({ name }) => {
|
||||||
if (draftOnlyTagsSet.has(name)) {
|
if (draftOnlyTagsSet.has(name)) {
|
||||||
console.log(`Omitting draft-only tag "${name}"`);
|
// console.log(`Omitting draft-only tag "${name}"`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (tagsSet.has(name)) {
|
uncategorizedTagsSet.delete(name);
|
||||||
uncategorizedTagsSet.delete(name);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { GetStaticPaths } from "astro";
|
import type { GetStaticPaths } from "astro";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import { type CollectionEntry, getCollection, type CollectionKey } from "astro:content";
|
import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content";
|
||||||
import { Markdown } from "@astropub/md";
|
import { Markdown } from "@astropub/md";
|
||||||
import { slug } from "github-slugger";
|
import { slug } from "github-slugger";
|
||||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||||
|
@ -45,24 +45,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
const tagDescriptions = tagCategories.reduce(
|
const tagDescriptions = tagCategories.reduce(
|
||||||
(acc, category) => {
|
(acc, category) => {
|
||||||
category.data.tags.forEach(({ name, description, related }) => {
|
category.data.tags.forEach(({ name, description, related }) => {
|
||||||
if (related) {
|
related = related.filter((relatedTag) => {
|
||||||
related = related.filter((relatedTag) => {
|
if (relatedTag == name) {
|
||||||
if (relatedTag == name) {
|
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
||||||
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
if (!tags.has(relatedTag)) {
|
||||||
if (!tags.has(relatedTag)) {
|
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
||||||
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
});
|
||||||
});
|
const key = typeof name === "string" ? name : name.en;
|
||||||
}
|
|
||||||
const key = typeof name === "string" ? name : name["eng"];
|
|
||||||
if (key in acc) {
|
if (key in acc) {
|
||||||
throw new Error(`Duplicated tag "${key}" found in multiple tag-categories lists`);
|
throw new Error(`Duplicated tag "${key}" found in multiple tag-categories lists`);
|
||||||
}
|
}
|
||||||
acc[key] = { description, related };
|
acc[key] = { description, related: related.length > 0 ? related : undefined };
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,14 +2,15 @@ import type { CollectionEntry } from "astro:content";
|
||||||
import { DEFAULT_LANG, type Lang } from "../content/config";
|
import { DEFAULT_LANG, type Lang } from "../content/config";
|
||||||
|
|
||||||
export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang): string {
|
export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang): string {
|
||||||
if (user.data.lang) {
|
const { name } = user.data;
|
||||||
if (user.data.lang[lang]) {
|
if (typeof name === "object") {
|
||||||
return user.data.lang[lang];
|
if (name[lang]) {
|
||||||
|
return name[lang];
|
||||||
}
|
}
|
||||||
throw new Error(`No "${lang}" translation for username "${user.data.name}" ("${user.id}")`);
|
throw new Error(`No "${lang}" translation for user "${user.id}"`);
|
||||||
}
|
}
|
||||||
if (lang !== DEFAULT_LANG) {
|
if (lang !== DEFAULT_LANG) {
|
||||||
console.warn(`User "${user.data.name}" ("${user.id}") has no "lang" property for a "${lang}" translation`);
|
console.warn(`Name "${name}" for user "${user.id}" isn't translated to "${lang}"; using default`);
|
||||||
}
|
}
|
||||||
return user.data.name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { ANONYMOUS_USER_ID as ID } from "../content/config";
|
import { ANONYMOUS_USER_ID } from "../content/config";
|
||||||
|
|
||||||
const ANONYMOUS_USER_ID: CollectionEntry<"users">["id"] = ID;
|
const ID: CollectionEntry<"users">["id"] = ANONYMOUS_USER_ID;
|
||||||
|
|
||||||
export const isAnonymousUser = (user: CollectionEntry<"users">) => user.id == ANONYMOUS_USER_ID;
|
export const isAnonymousUser = (user: CollectionEntry<"users">) => user.id === ID;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict"
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"exclude": ["dist"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue