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": {
|
||||
"*.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]": {
|
||||
"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
|
||||
|
||||
- Node.js 20+
|
||||
- (optional) LFTP, for the remote deployment script.
|
||||
- (optional) rsync, for remote deployment.
|
||||
- (optional) LibreOffice, for the story export script.
|
||||
|
||||
## Development
|
||||
|
@ -31,7 +31,7 @@ npm run prettier # Prettier formatting
|
|||
Requires `libreoffice` to be installed and in your path.
|
||||
|
||||
```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
|
||||
|
@ -40,19 +40,8 @@ npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-story-to-exp
|
|||
npm run build
|
||||
```
|
||||
|
||||
Then, if you're using LFTP:
|
||||
|
||||
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:
|
||||
Then, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`:
|
||||
|
||||
```bash
|
||||
npm run deploy-lftp
|
||||
rsync --delete -acP dist/ gallerybm:/home/public
|
||||
```
|
||||
|
|
|
@ -19,6 +19,7 @@ export default defineConfig({
|
|||
build: {
|
||||
assets: "assets",
|
||||
},
|
||||
outDir: "./dist",
|
||||
redirects: {
|
||||
"/stories": "/stories/1",
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ tags: []
|
|||
# series: the-lost-of-the-marshes
|
||||
# relatedStories: []
|
||||
# relatedGames: []
|
||||
# lang: eng
|
||||
# lang: en
|
||||
---
|
||||
|
||||
The game content (i.e. embed) goes here.
|
||||
|
|
|
@ -25,7 +25,7 @@ tags: []
|
|||
# Some funny summary
|
||||
# relatedStories: []
|
||||
# relatedGames: []
|
||||
# lang: eng
|
||||
# lang: en
|
||||
---
|
||||
|
||||
The story goes here.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: Nameless User
|
||||
nameLang:
|
||||
eng: Nameless
|
||||
en: Nameless
|
||||
tok: jan Nenle pi nimi ala
|
||||
# avatar: /src/assets/images/logo_bm.png
|
||||
links:
|
||||
|
|
307
package-lock.json
generated
307
package-lock.json
generated
|
@ -1,26 +1,26 @@
|
|||
{
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.8.2",
|
||||
"@astrojs/check": "^0.9.2",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astropub/md": "^1.0.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"astro": "^4.12.2",
|
||||
"astro": "^4.13.1",
|
||||
"astro-pagefind": "^1.6.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"marked": "^12.0.1",
|
||||
"marked": "^12.0.2",
|
||||
"pagefind": "^1.1.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"tiny-decode": "^0.1.3",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
|
@ -32,7 +32,7 @@
|
|||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"tsx": "^4.16.2"
|
||||
"tsx": "^4.16.5"
|
||||
}
|
||||
},
|
||||
"../astro-pagefind/packages/astro-pagefind": {
|
||||
|
@ -83,12 +83,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@astrojs/check": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.8.2.tgz",
|
||||
"integrity": "sha512-L0V9dGb2PGvK9Mf3kby99Y+qm7EqxaC9tN1MVCvaqp/3pPPZBadR4XAySHipxXqQsxwJS25WQow8/1kMl1e25g==",
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.2.tgz",
|
||||
"integrity": "sha512-6rWxtJTbd/ctdAlmla0CAvloGaai5IUTG0K21kctJHHGKJKnGH6Xana7m0zNOtHpVPEJi1SgC/TcsN+ltYt0Cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/language-server": "^2.12.1",
|
||||
"@astrojs/language-server": "^2.13.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"fast-glob": "^3.3.1",
|
||||
"kleur": "^4.1.5",
|
||||
|
@ -102,9 +102,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@astrojs/compiler": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.9.2.tgz",
|
||||
"integrity": "sha512-Vpu0Ffsj8SoV+N0DFHlxxOMKHwSC9059Xy/OlG1t6uFYSoJXxkBC2WyF6igO7x10V+8uJrhOxaXr3nA90kJXow==",
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.10.2.tgz",
|
||||
"integrity": "sha512-bvH+v8AirwpRWCkYJEyWYdc5Cs/BjG2ZTxIJzttHilXgfKJAdW2496KsUQKzf5j2tOHtaHXKKn9hb9WZiBGpEg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@astrojs/internal-helpers": {
|
||||
|
@ -114,12 +114,12 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@astrojs/language-server": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.12.1.tgz",
|
||||
"integrity": "sha512-CCibE6XwSmrZEKlPDr48LZJN7NWxOurOJK1yOzqZFMNV8Y6DIqF6s1e60gbNNHMZkthWYBNTPno4Ni/XyviinQ==",
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.13.2.tgz",
|
||||
"integrity": "sha512-l435EZLKjaUO/6iewJ7xqd3eHf3zAosVWG4woILbxluQcianBoNPepnnqAg7uUriZUaC44ae5v0Q+AfB8UI64g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.9.1",
|
||||
"@astrojs/compiler": "^2.10.2",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@volar/kit": "~2.4.0-alpha.15",
|
||||
"@volar/language-core": "~2.4.0-alpha.15",
|
||||
|
@ -256,30 +256,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz",
|
||||
"integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==",
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
|
||||
"integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
|
||||
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.24.9",
|
||||
"@babel/helper-compilation-targets": "^7.24.8",
|
||||
"@babel/helper-module-transforms": "^7.24.9",
|
||||
"@babel/helpers": "^7.24.8",
|
||||
"@babel/parser": "^7.24.8",
|
||||
"@babel/template": "^7.24.7",
|
||||
"@babel/traverse": "^7.24.8",
|
||||
"@babel/types": "^7.24.9",
|
||||
"@babel/generator": "^7.25.0",
|
||||
"@babel/helper-compilation-targets": "^7.25.2",
|
||||
"@babel/helper-module-transforms": "^7.25.2",
|
||||
"@babel/helpers": "^7.25.0",
|
||||
"@babel/parser": "^7.25.0",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/traverse": "^7.25.2",
|
||||
"@babel/types": "^7.25.2",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
|
@ -304,12 +304,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz",
|
||||
"integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
|
||||
"integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.24.9",
|
||||
"@babel/types": "^7.25.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^2.5.1"
|
||||
|
@ -331,12 +331,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz",
|
||||
"integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==",
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
|
||||
"integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.24.8",
|
||||
"@babel/compat-data": "^7.25.2",
|
||||
"@babel/helper-validator-option": "^7.24.8",
|
||||
"browserslist": "^4.23.1",
|
||||
"lru-cache": "^5.1.1",
|
||||
|
@ -355,43 +355,6 @@
|
|||
"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": {
|
||||
"version": "7.24.7",
|
||||
"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": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz",
|
||||
"integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==",
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
|
||||
"integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.24.7",
|
||||
"@babel/helper-module-imports": "^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": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -446,18 +408,6 @@
|
|||
"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": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
|
||||
|
@ -486,13 +436,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz",
|
||||
"integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
|
||||
"integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.24.7",
|
||||
"@babel/types": "^7.24.8"
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/types": "^7.25.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -514,10 +464,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz",
|
||||
"integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==",
|
||||
"version": "7.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
|
||||
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.25.2"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
|
@ -541,16 +494,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz",
|
||||
"integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==",
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz",
|
||||
"integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^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/types": "^7.24.7"
|
||||
"@babel/types": "^7.25.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -560,33 +513,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
||||
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
|
||||
"integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@babel/types": "^7.24.7"
|
||||
"@babel/parser": "^7.25.0",
|
||||
"@babel/types": "^7.25.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz",
|
||||
"integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==",
|
||||
"version": "7.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
|
||||
"integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.24.8",
|
||||
"@babel/helper-environment-visitor": "^7.24.7",
|
||||
"@babel/helper-function-name": "^7.24.7",
|
||||
"@babel/helper-hoist-variables": "^7.24.7",
|
||||
"@babel/helper-split-export-declaration": "^7.24.7",
|
||||
"@babel/parser": "^7.24.8",
|
||||
"@babel/types": "^7.24.8",
|
||||
"@babel/generator": "^7.25.0",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/types": "^7.25.2",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
|
@ -595,9 +545,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
|
||||
"integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
|
||||
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.8",
|
||||
|
@ -1582,9 +1532,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
|
@ -1863,9 +1814,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-Qsn8h15SWgv5TDRoDmiHNzdQO2BxDe86Yq6vIHf5T0cCvmfmccJKIzHtep8bQO9HMBZYCtCBzaXdd1MnxZBPSg==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.12.1.tgz",
|
||||
"integrity": "sha512-biCz/mnkMktImI6hMfMX3H9kOeqsInxWEyCHbSlL8C/2TR1FqfmGxTLRNwYCKsyCyxWLbB8rEqXRVZuyxuLFmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.4"
|
||||
|
@ -2285,34 +2236,33 @@
|
|||
}
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.12.2.tgz",
|
||||
"integrity": "sha512-l6OmqlL+FiuSi9x6F+EGZitteOznq1JffOil7st7cdqeMCTEIym4oagI1a6zp6QekliKWEEZWdplGhgh1k1f7Q==",
|
||||
"version": "4.13.1",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.13.1.tgz",
|
||||
"integrity": "sha512-VnMjAc+ykFsIVjgbu9Mt/EA1fMIcPMXbU89h3ATwGOzSIKDsQH72bDgfJkWiwk6u0OE90GeP5EPhAT28Twf9oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.9.0",
|
||||
"@astrojs/compiler": "^2.10.0",
|
||||
"@astrojs/internal-helpers": "0.4.1",
|
||||
"@astrojs/markdown-remark": "5.2.0",
|
||||
"@astrojs/telemetry": "3.1.0",
|
||||
"@babel/core": "^7.24.9",
|
||||
"@babel/generator": "^7.24.10",
|
||||
"@babel/parser": "^7.24.8",
|
||||
"@babel/plugin-transform-react-jsx": "^7.24.7",
|
||||
"@babel/traverse": "^7.24.8",
|
||||
"@babel/types": "^7.24.9",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/generator": "^7.25.0",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.25.2",
|
||||
"@babel/traverse": "^7.25.3",
|
||||
"@babel/types": "^7.25.2",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.0",
|
||||
"axobject-query": "^4.1.0",
|
||||
"boxen": "7.1.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"ci-info": "^4.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"common-ancestor-path": "^1.0.1",
|
||||
"cookie": "^0.6.0",
|
||||
"cssesc": "^3.0.0",
|
||||
"debug": "^4.3.5",
|
||||
"debug": "^4.3.6",
|
||||
"deterministic-object-hash": "^2.0.2",
|
||||
"devalue": "^5.0.0",
|
||||
"diff": "^5.2.0",
|
||||
|
@ -2330,7 +2280,7 @@
|
|||
"http-cache-semantics": "^4.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.10",
|
||||
"magic-string": "^0.30.11",
|
||||
"mrmime": "^2.0.0",
|
||||
"ora": "^8.0.1",
|
||||
"p-limit": "^6.1.0",
|
||||
|
@ -2339,19 +2289,19 @@
|
|||
"preferred-pm": "^4.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"rehype": "^13.0.1",
|
||||
"semver": "^7.6.2",
|
||||
"shiki": "^1.10.3",
|
||||
"semver": "^7.6.3",
|
||||
"shiki": "^1.12.0",
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tsconfck": "^3.1.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.2",
|
||||
"vite": "^5.3.4",
|
||||
"vite": "^5.3.5",
|
||||
"vitefu": "^0.2.5",
|
||||
"which-pm": "^3.0.0",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.1"
|
||||
"zod-to-json-schema": "^3.23.2"
|
||||
},
|
||||
"bin": {
|
||||
"astro": "astro.js"
|
||||
|
@ -2958,9 +2908,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
|
@ -4270,12 +4220,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.10",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
|
||||
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
|
||||
"version": "0.30.11",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
|
||||
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
|
@ -4295,9 +4245,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz",
|
||||
"integrity": "sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==",
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
|
@ -6336,12 +6287,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.11.1.tgz",
|
||||
"integrity": "sha512-VHD3Q0EBXaaa245jqayBe5zQyMQUdXBFjmGr9MpDaDpAKRMYn7Ff00DM5MLk26UyKjnml3yQ0O2HNX7PtYVNFQ==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.12.1.tgz",
|
||||
"integrity": "sha512-nwmjbHKnOYYAe1aaQyEBHvQymJgfm86ZSS7fT8OaPRr4sbAcBNz7PbfAikMEFSDQ6se2j2zobkXvVKcBOm0ysg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "1.11.1",
|
||||
"@shikijs/core": "1.12.1",
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
},
|
||||
|
@ -6616,9 +6567,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
|
||||
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
||||
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
|
@ -6838,9 +6789,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.16.2",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz",
|
||||
"integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==",
|
||||
"version": "4.16.5",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz",
|
||||
"integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -7122,9 +7073,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
|
||||
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
|
||||
"version": "5.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
|
||||
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
|
@ -7363,9 +7314,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-textdocument": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz",
|
||||
"integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==",
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
||||
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
|
@ -7716,9 +7667,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.23.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.1.tgz",
|
||||
"integrity": "sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==",
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz",
|
||||
"integrity": "sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.3"
|
||||
|
|
17
package.json
17
package.json
|
@ -1,33 +1,32 @@
|
|||
{
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"type": "module",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "npm run check && astro build",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"sync": "astro sync",
|
||||
"check": "astro check --minimumSeverity warning",
|
||||
"check": "astro check",
|
||||
"astro": "astro",
|
||||
"prettier": "prettier --write .",
|
||||
"deploy-lftp": "dotenv tsx scripts/deploy-lftp.ts --",
|
||||
"export-story": "tsx scripts/export-story.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.8.2",
|
||||
"@astrojs/check": "^0.9.2",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astropub/md": "^1.0.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"astro": "^4.12.2",
|
||||
"astro": "^4.13.1",
|
||||
"astro-pagefind": "^1.6.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"marked": "^12.0.1",
|
||||
"marked": "^12.0.2",
|
||||
"pagefind": "^1.1.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"tiny-decode": "^0.1.3",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
|
@ -39,6 +38,6 @@
|
|||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"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 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 the CC-BY-4.0 license.
|
||||
The generic SVG icons were created by Font Awesome and are distributed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/
|
||||
|
||||
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 { tmpdir } from "node:os";
|
||||
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("*", "\\*"));
|
||||
const tempDir = await mkdtemp(pathJoin(tmpdir(), "export-story-"));
|
||||
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 rtfStyles = getRTFStyles(rtfText);
|
||||
await writeFile(
|
||||
|
@ -158,7 +164,6 @@ async function exportStory(slug: string, options: { outputDir: string }) {
|
|||
rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
|
||||
);
|
||||
console.log("Success!");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await program
|
||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 22 KiB After ![]() (image error) Size: 20 KiB ![]() ![]() |
|
@ -54,11 +54,13 @@
|
|||
(function () {
|
||||
if (localStorage.getItem("ageVerified") !== "true") {
|
||||
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 rejectButton = modal.querySelector<HTMLButtonElement>("button[data-modal-reject]")!;
|
||||
const acceptButton = modal.querySelector<HTMLButtonElement>("button[data-modal-accept]")!;
|
||||
const modal = document.querySelector<HTMLElementTagNameMap["div"]>("body > div#modal-age-restricted")!;
|
||||
const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
|
||||
const acceptButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!;
|
||||
function onRejectButtonClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
location.href = "about:blank";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { type Lang } from "../content/config";
|
||||
import type { Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
|
||||
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";
|
||||
|
||||
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 Lang } from "../content/config";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import type { Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
import UserComponent from "./UserComponent.astro";
|
||||
import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
|
||||
|
@ -15,7 +15,7 @@ const { copyrightedCharacters, lang } = Astro.props;
|
|||
|
||||
{
|
||||
copyrightedCharacters ? (
|
||||
<section id="copyrighted-characters">
|
||||
<section id="copyrighted-characters" aria-label={t(lang, "characters/copyrighted_characters_aria_label")}>
|
||||
<ul>
|
||||
{copyrightedCharacters.map(([owner, characterList]) => (
|
||||
<CopyrightedCharactersItem
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
<script>
|
||||
(function () {
|
||||
var colorScheme = localStorage.getItem("colorScheme");
|
||||
let colorScheme = localStorage.getItem("colorScheme");
|
||||
if (colorScheme == null || colorScheme === "auto") {
|
||||
colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
document.querySelectorAll("button[data-dark-mode]").forEach(function (button) {
|
||||
button.addEventListener("click", function (e) {
|
||||
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
|
||||
button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
if (colorScheme === "dark") {
|
||||
colorScheme = "light";
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
---
|
||||
import type { Lang } from "../content/config";
|
||||
|
||||
type Props = {
|
||||
instance?: string;
|
||||
user?: string;
|
||||
postId?: string;
|
||||
lang: Lang;
|
||||
link: string;
|
||||
instance: string;
|
||||
user: string;
|
||||
postId: string;
|
||||
};
|
||||
|
||||
const { instance, user, postId } = Astro.props;
|
||||
const { link, instance, user, postId } = Astro.props;
|
||||
---
|
||||
|
||||
<section
|
||||
id="comment-section"
|
||||
class="hidden px-2 font-serif"
|
||||
class="px-2 font-serif"
|
||||
aria-describedby="title-comment-section"
|
||||
data-instance={instance || ""}
|
||||
data-user={user || ""}
|
||||
data-post-id={postId || ""}
|
||||
data-link={link}
|
||||
data-instance={instance}
|
||||
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">
|
||||
Comments
|
||||
</h2>
|
||||
<div class="text-stone-800 dark:text-stone-100" id="comments">
|
||||
<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"
|
||||
data-load-comments
|
||||
>
|
||||
<span>Click to load comments</span>
|
||||
</button>
|
||||
<p class="my-1">
|
||||
<a class="text-link underline" href={link} target="_blank">View comments on Mastodon</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template id="template-comments-loading">
|
||||
<svg class="-mt-1 mr-1 inline h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<template id="template-button">
|
||||
<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>
|
||||
<path
|
||||
class="opacity-100"
|
||||
|
@ -45,12 +55,16 @@ const { instance, user, postId } = Astro.props;
|
|||
<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="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" />
|
||||
<span data-display-name></span>
|
||||
</a>
|
||||
<a data-post-link class="text-link my-1 flex items-center text-sm font-light hover:underline focus:underline">
|
||||
<span class="mr-1" data-publish-date></span>
|
||||
<a
|
||||
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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div data-comment-thread class="-mb-2"></div>
|
||||
<div data-comment-thread class="-mb-2" aria-hidden></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
interface Post {
|
||||
link: string;
|
||||
instance: string;
|
||||
user: string;
|
||||
postId: string;
|
||||
}
|
||||
|
||||
interface Emoji {
|
||||
shortcode: string;
|
||||
url: string;
|
||||
|
@ -104,115 +125,131 @@ const { instance, user, postId } = Astro.props;
|
|||
emojis: Emoji[];
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
ancestors: Comment[];
|
||||
descendants: Comment[];
|
||||
}
|
||||
|
||||
(function () {
|
||||
const replaceEmojis = (text: string, emojis: Emoji[], imgClass: string) =>
|
||||
emojis.reduce(
|
||||
function replaceEmojis(text: string, emojis: Emoji[]) {
|
||||
return emojis.reduce(
|
||||
(acc, emoji) =>
|
||||
acc.replaceAll(
|
||||
`:${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,
|
||||
);
|
||||
}
|
||||
|
||||
const commentSection = document.querySelector<Element>("#comment-section")!;
|
||||
const instance = commentSection.getAttribute("data-instance");
|
||||
const user = commentSection.getAttribute("data-user");
|
||||
const postId = commentSection.getAttribute("data-post-id");
|
||||
if (instance && user && postId) {
|
||||
commentSection.classList.remove("hidden");
|
||||
commentSection.querySelector<HTMLButtonElement>("button[data-load-comments]")!.addEventListener("click", (e) => {
|
||||
async function renderComments(section: Element, post: Post) {
|
||||
const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
|
||||
try {
|
||||
const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
|
||||
}
|
||||
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();
|
||||
const loadCommentsButton = e.target as HTMLButtonElement;
|
||||
loadCommentsButton.setAttribute("disabled", "true");
|
||||
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 () => {
|
||||
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();
|
||||
renderComments(commentSection, post as Post);
|
||||
});
|
||||
}
|
||||
|
||||
initCommentSection();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { type Lang } from "../content/config";
|
||||
import type { Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
|
||||
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 Lang } from "../content/config";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import type { Lang } from "../content/config";
|
||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||
|
||||
type Props = {
|
||||
|
@ -12,12 +12,7 @@ let { user, lang } = Astro.props;
|
|||
const username = getUsernameForLang(user, lang);
|
||||
let link: string | null = null;
|
||||
if (user.data.preferredLink) {
|
||||
const preferredLink = user.data.links[user.data.preferredLink]!;
|
||||
if (typeof preferredLink === "string") {
|
||||
link = preferredLink;
|
||||
} else {
|
||||
link = preferredLink[0];
|
||||
}
|
||||
link = user.data.links[user.data.preferredLink]!.link;
|
||||
}
|
||||
---
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../assets/LICENSE
|
|
@ -2,89 +2,214 @@ import { defineCollection, reference, z } from "astro:content";
|
|||
|
||||
// Constants
|
||||
|
||||
export const WEBSITE_LIST = [
|
||||
"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 DEFAULT_LANG = "en";
|
||||
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
|
||||
|
||||
const trimText = (text: string) => text.trim();
|
||||
const adjustDateForUTCOffset = (date: Date) =>
|
||||
new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
|
||||
const parseMastodonPostUrl = (url: string, ctx: z.RefinementCtx) => {
|
||||
const match = mastodonPostUrlRegex.exec(url);
|
||||
if (!match) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `"mastodon" post contains an invalid URL`,
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return {
|
||||
instance: match[1]!,
|
||||
user: match[2]!,
|
||||
postId: match[3]!,
|
||||
function parseRegex<R extends { [key: string]: string }>(regex: RegExp) {
|
||||
return (url: string, ctx: z.RefinementCtx) => {
|
||||
const match = regex.exec(url);
|
||||
if (!match?.groups) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `"${ctx.path}" did not match regex`,
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return match.groups as R;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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 Website = z.infer<typeof website>;
|
||||
export type Website = keyof z.input<typeof websiteLinks>;
|
||||
export type GamePlatform = z.infer<typeof platform>;
|
||||
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
||||
|
||||
|
@ -93,79 +218,52 @@ export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
|
|||
const storiesCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
// Required
|
||||
title: z.string(),
|
||||
wordCount: z.number().int().optional(),
|
||||
contentWarning: z.string().transform(trimText),
|
||||
description: z.string().transform(trimText),
|
||||
tags: z.array(z.string()),
|
||||
// Optional
|
||||
pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
|
||||
isDraft: z.boolean().default(false),
|
||||
shortTitle: z.string().optional(),
|
||||
authors,
|
||||
summary: z.string().transform(trimText).optional(),
|
||||
thumbnail: image().optional(),
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
series: reference("series").optional(),
|
||||
commissioner: userList.optional(),
|
||||
requester: userList.optional(),
|
||||
copyrightedCharacters: copyrightedCharacters,
|
||||
lang,
|
||||
prev: reference("stories").nullish(),
|
||||
next: reference("stories").nullish(),
|
||||
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({}),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
// Required parameters, but optional for drafts (isDraft == true)
|
||||
wordCount: z.number().int().optional(),
|
||||
thumbnail: image().optional(),
|
||||
// Optional parameters
|
||||
shortTitle: z.string().optional(),
|
||||
commissioner: userList.optional(),
|
||||
requester: userList.optional(),
|
||||
summary: z.string().trim().optional(),
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
prev: reference("stories").nullish(),
|
||||
next: reference("stories").nullish(),
|
||||
})
|
||||
.and(publishedContent)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published story`)
|
||||
.refine(
|
||||
({ isDraft, contentWarning }) => isDraft || contentWarning,
|
||||
`Missing "contentWarning" for published story`,
|
||||
)
|
||||
.refine(({ isDraft, wordCount }) => isDraft || wordCount, `Missing "wordCount" for published story`)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published story`)
|
||||
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
|
||||
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published story`),
|
||||
});
|
||||
|
||||
const gamesCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
// Required
|
||||
title: z.string(),
|
||||
contentWarning: z.string().transform(trimText),
|
||||
description: z.string().transform(trimText),
|
||||
platforms: z.array(platform).refine((platforms) => platforms.length > 0, `"platforms" cannot be empty`),
|
||||
tags: z.array(z.string()),
|
||||
// Optional
|
||||
pubDate: z.date().transform(adjustDateForUTCOffset).optional(),
|
||||
isDraft: z.boolean().default(false),
|
||||
authors,
|
||||
thumbnail: image().optional(),
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
series: reference("series").optional(),
|
||||
copyrightedCharacters: copyrightedCharacters,
|
||||
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({}),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
// Required parameters, but optional for drafts (isDraft == true)
|
||||
platforms: z.array(platform).default([]),
|
||||
thumbnail: image().optional(),
|
||||
// Optional parameters
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
})
|
||||
.and(publishedContent)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published game`)
|
||||
.refine(({ isDraft, contentWarning }) => isDraft || contentWarning, `Missing "contentWarning" for published game`)
|
||||
.refine(({ isDraft, platforms }) => isDraft || platforms.length, `Missing "platforms" for published game`)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published game`)
|
||||
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published game`)
|
||||
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published game`),
|
||||
});
|
||||
|
||||
// Data collections
|
||||
|
@ -175,12 +273,11 @@ const usersCollection = defineCollection({
|
|||
schema: ({ image }) =>
|
||||
z
|
||||
.object({
|
||||
// Required
|
||||
name: z.string(),
|
||||
links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])),
|
||||
// Optional
|
||||
preferredLink: website.nullish(),
|
||||
lang: langRecord.optional(),
|
||||
// Required parameters
|
||||
name: langRecord.or(z.string()),
|
||||
links: websiteLinks.partial(),
|
||||
// Optional parameters
|
||||
preferredLink: websiteLinks.keyof().nullish(),
|
||||
avatar: image().optional(),
|
||||
})
|
||||
.refine(
|
||||
|
@ -195,7 +292,7 @@ const usersCollection = defineCollection({
|
|||
const seriesCollection = defineCollection({
|
||||
type: "data",
|
||||
schema: z.object({
|
||||
// Required
|
||||
// Required parameters
|
||||
name: z.string(),
|
||||
url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
|
||||
}),
|
||||
|
@ -204,16 +301,18 @@ const seriesCollection = defineCollection({
|
|||
const tagCategoriesCollection = defineCollection({
|
||||
type: "data",
|
||||
schema: z.object({
|
||||
// Required
|
||||
// Required parameters
|
||||
name: z.string(),
|
||||
index: z.number().int(),
|
||||
tags: z.array(
|
||||
z.object({
|
||||
name: z.union([z.string(), langRecord]),
|
||||
description: z.string().optional(),
|
||||
related: z.array(z.string()).optional(),
|
||||
}),
|
||||
),
|
||||
tags: z
|
||||
.array(
|
||||
z.object({
|
||||
name: langRecord.or(z.string()),
|
||||
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
|
||||
sofurry: https://www.sofurry.com/view/2109688
|
||||
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
|
||||
tags:
|
||||
- oral vore
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
slug: playing-it-safe
|
||||
title: Playing It Safe
|
||||
pubDate: 2024-08-08
|
||||
isDraft: true
|
||||
authors: bad-manners
|
||||
wordCount: 9900
|
||||
contentWarning: >
|
||||
|
|
|
@ -16,6 +16,7 @@ posts:
|
|||
inkbunny: https://inkbunny.net/s/3283508
|
||||
sofurry: https://www.sofurry.com/view/2118138
|
||||
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
|
||||
tags:
|
||||
- anthro predator
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: Types of vore
|
||||
index: 1
|
||||
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.
|
||||
- name: anal vore
|
||||
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.
|
||||
- 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.
|
||||
- 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.
|
||||
- name: human prey
|
||||
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".
|
||||
- name: feral prey
|
||||
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.
|
||||
|
|
|
@ -13,7 +13,7 @@ tags:
|
|||
description: Scenarios where at least one of the predators is a woman and/or female-presenting.
|
||||
- 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.
|
||||
- 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.
|
||||
- name: male prey
|
||||
description: Scenarios where at least one of the prey is a man and/or male-presenting.
|
||||
|
@ -27,5 +27,5 @@ tags:
|
|||
- female 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.
|
||||
- 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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: Willingness
|
||||
index: 5
|
||||
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.
|
||||
- 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.
|
||||
|
@ -13,7 +13,7 @@ tags:
|
|||
description: Scenarios where at least one of the prey participates in vore willingly.
|
||||
- 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.
|
||||
- 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.
|
||||
- name: asleep prey
|
||||
description: Scenarios where at least one of the predators participates in vore while asleep.
|
||||
|
|
|
@ -66,4 +66,4 @@ tags:
|
|||
- name: soul vore
|
||||
description: Scenarios where predators consume a soul instead of their prey's body.
|
||||
- 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.
|
||||
- name: commission
|
||||
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.
|
||||
- name: toki pona
|
||||
description: Stories written in toki pona, the language of good.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
name: Anonymous
|
||||
lang:
|
||||
eng: anonymous
|
||||
name:
|
||||
en: anonymous
|
||||
tok: jan pi nimi ala
|
||||
links: {}
|
||||
preferredLink: ~
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: Asof Yeun
|
||||
links:
|
||||
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
|
||||
sofurry: https://asofyeun.sofurry.com/
|
||||
weasyl: https://www.weasyl.com/~asofyeun
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
name: Bad Manners
|
||||
lang:
|
||||
eng: Bad Manners
|
||||
name:
|
||||
en: Bad Manners
|
||||
tok: nasin ike Pemene
|
||||
avatar: /src/assets/images/logo_bm.png
|
||||
links:
|
||||
|
@ -9,8 +8,8 @@ links:
|
|||
furaffinity: https://www.furaffinity.net/user/BadManners
|
||||
inkbunny: https://inkbunny.net/BadManners
|
||||
sofurry:
|
||||
- https://bad-manners.sofurry.com/
|
||||
- Bad Manners
|
||||
link: https://bad-manners.sofurry.com/
|
||||
username: Bad Manners
|
||||
weasyl: https://www.weasyl.com/~BadManners
|
||||
twitter: https://twitter.com/BadManners__
|
||||
mastodon: https://meow.social/@BadManners
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: Dr. Hans Woofington
|
||||
links:
|
||||
furaffinity:
|
||||
- https://www.furaffinity.net/user/hanslewdington/
|
||||
- Hans_Lewdington
|
||||
link: https://www.furaffinity.net/user/hanslewdington/
|
||||
username: Hans_Lewdington
|
||||
preferredLink: furaffinity
|
||||
|
|
|
@ -2,6 +2,6 @@ name: YolkMonkey
|
|||
links:
|
||||
furaffinity: https://furaffinity.net/user/Vampire101
|
||||
sofurry:
|
||||
- https://vampire101.sofurry.com/
|
||||
- Vampire101
|
||||
link: https://vampire101.sofurry.com/
|
||||
username: Vampire101
|
||||
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";
|
||||
export { DEFAULT_LANG } from "../content/config";
|
||||
|
||||
const UI_STRINGS = {
|
||||
// Utility functions
|
||||
"util/join_names": {
|
||||
eng: (names: string[]) =>
|
||||
en: (names: string[]) =>
|
||||
names.length <= 1
|
||||
? names.join("")
|
||||
: names.length == 2
|
||||
|
@ -13,10 +14,10 @@ const UI_STRINGS = {
|
|||
tok: (names: string[]) => names.join(" en "),
|
||||
},
|
||||
"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": {
|
||||
eng: (count: number, nounSingular: string, nounPlural: string | undefined) => {
|
||||
en: (count: number, nounSingular: string, nounPlural?: string) => {
|
||||
if (count == 0) {
|
||||
return `no ${nounPlural ?? nounSingular}`;
|
||||
}
|
||||
|
@ -25,146 +26,205 @@ const UI_STRINGS = {
|
|||
}
|
||||
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"}`,
|
||||
},
|
||||
"export_story/writing": {
|
||||
eng: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
|
||||
// export-story API functions
|
||||
"export_story/authors": {
|
||||
en: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
|
||||
tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
|
||||
},
|
||||
"export_story/request_for": {
|
||||
eng: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
|
||||
en: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
|
||||
},
|
||||
"export_story/commissioned_by": {
|
||||
eng: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
|
||||
en: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
|
||||
},
|
||||
"story/return_to_stories": {
|
||||
eng: "Return to stories",
|
||||
tok: "o tawa e lipu ale",
|
||||
// Shared strings for published content (stories + games)
|
||||
"published_content/return_to_series": {
|
||||
en: (seriesName: string) => `Return to ${seriesName}`,
|
||||
},
|
||||
"story/return_to_series": {
|
||||
eng: (seriesName: string) => `Return to ${seriesName}`,
|
||||
},
|
||||
"story/go_to_description": {
|
||||
eng: "Go to description",
|
||||
"published_content/go_to_description": {
|
||||
en: "Go to description",
|
||||
tok: "o tawa e toki lipu",
|
||||
},
|
||||
"story/toggle_dark_mode": {
|
||||
eng: "Toggle dark mode",
|
||||
"published_content/toggle_dark_mode": {
|
||||
en: "Toggle dark mode",
|
||||
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": {
|
||||
eng: (wordCount: number | string | undefined, contentWarning: string) =>
|
||||
en: (wordCount: number | string | undefined, contentWarning: string) =>
|
||||
wordCount ? `Word count: ${wordCount}. ${contentWarning}` : contentWarning,
|
||||
tok: (_wordCount: number | string | undefined, contentWarning: string) => contentWarning,
|
||||
},
|
||||
"story/publish_date": {
|
||||
eng: (date: string) => date,
|
||||
tok: (date: string) => `tenpo suno ${date}`,
|
||||
},
|
||||
"story/description": {
|
||||
eng: "Description",
|
||||
tok: "toki lipu",
|
||||
"story/article_aria_label": {
|
||||
en: "Story",
|
||||
tok: "lipu",
|
||||
},
|
||||
"story/summary": {
|
||||
eng: "Summary",
|
||||
en: "Summary",
|
||||
tok: "lipu tawa tenpo lili",
|
||||
},
|
||||
"story/reveal_summary": {
|
||||
eng: "Click to reveal",
|
||||
en: "Click to reveal",
|
||||
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": {
|
||||
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[]) =>
|
||||
authorsList.length > 1
|
||||
? `lipu ni li tan jan ni: ${UI_STRINGS["util/join_names"].tok(authorsList)}`
|
||||
: `lipu ni li tan ${authorsList[0]}`,
|
||||
},
|
||||
"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": {
|
||||
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": {
|
||||
eng: "DRAFT VERSION – DO NOT REDISTRIBUTE",
|
||||
// Game page-specific strings
|
||||
"game/return_to_games": {
|
||||
en: "Return to games",
|
||||
},
|
||||
"characters/characters_are_copyrighted_by": {
|
||||
eng: (owner: string, charactersList: string[]) =>
|
||||
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/title_aria_label": {
|
||||
en: "Game title",
|
||||
},
|
||||
"game/platforms": {
|
||||
eng: (platforms: GamePlatform[]) => {
|
||||
en: (platforms: GamePlatform[]) => {
|
||||
if (platforms.length == 0) {
|
||||
return "";
|
||||
}
|
||||
const translatedPlatforms = platforms.map((platform) => {
|
||||
const platformLang = UI_STRINGS[`game/platform_${platform}`].eng;
|
||||
const platformLang = UI_STRINGS[`game/platform_${platform}`].en;
|
||||
if (!platformLang) {
|
||||
throw new Error(`Invalid platform "${platform}"`);
|
||||
}
|
||||
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": {
|
||||
eng: "web browsers",
|
||||
en: "web browsers",
|
||||
},
|
||||
"game/platform_windows": {
|
||||
eng: "Windows",
|
||||
en: "Windows",
|
||||
},
|
||||
"game/platform_linux": {
|
||||
eng: "Linux",
|
||||
en: "Linux",
|
||||
},
|
||||
"game/platform_macos": {
|
||||
eng: "macOS",
|
||||
en: "macOS",
|
||||
},
|
||||
"game/platform_android": {
|
||||
eng: "Android",
|
||||
en: "Android",
|
||||
},
|
||||
"game/platform_ios": {
|
||||
eng: "iOS",
|
||||
en: "iOS",
|
||||
},
|
||||
"game/warnings": {
|
||||
eng: (platforms: GamePlatform[], contentWarning: string) =>
|
||||
platforms.length > 0 ? `${UI_STRINGS["game/platforms"].eng(platforms)} ${contentWarning}` : contentWarning,
|
||||
en: (platforms: GamePlatform[], contentWarning: string) =>
|
||||
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": {
|
||||
eng: (tag: string, storiesCount: number, gamesCount: number) => {
|
||||
en: (tag: string, storiesCount: number, gamesCount: number) => {
|
||||
const content = [];
|
||||
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) {
|
||||
content.push(UI_STRINGS["util/enumerate"].eng(gamesCount, "game", "games"));
|
||||
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
|
||||
}
|
||||
if (content.length == 0) {
|
||||
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;
|
||||
|
@ -176,6 +236,9 @@ type TranslationEntry<T> = { [DEFAULT_LANG]: T } & {
|
|||
type TranslationArgs<K extends TranslationKey> =
|
||||
(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 {
|
||||
if (key in UI_STRINGS) {
|
||||
const translation: string | ((...args: TranslationArgs<K>) => string) =
|
||||
|
|
|
@ -6,12 +6,13 @@ import AgeRestrictedModal from "../components/AgeRestrictedModal.astro";
|
|||
|
||||
type Props = {
|
||||
pageTitle?: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
const { pageTitle } = Astro.props;
|
||||
const { pageTitle = "Gallery", lang = "en" } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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="alternate"
|
||||
|
|
|
@ -18,8 +18,8 @@ const { props } = Astro;
|
|||
const series = props.series && (await getEntry(props.series));
|
||||
const authorsList = await getEntries(props.authors);
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
||||
// const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.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 categorizedTags = Object.fromEntries(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
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 }));
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title}>
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<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]" />
|
||||
{
|
||||
thumbnail ? (
|
||||
|
@ -55,7 +55,7 @@ const thumbnail =
|
|||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
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]"
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -78,7 +78,9 @@ const thumbnail =
|
|||
<a
|
||||
href={series ? series.data.url : "/games"}
|
||||
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">
|
||||
<path
|
||||
|
@ -89,7 +91,7 @@ const thumbnail =
|
|||
<a
|
||||
href="#description"
|
||||
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">
|
||||
<path
|
||||
|
@ -100,7 +102,7 @@ const thumbnail =
|
|||
<button
|
||||
data-dark-mode
|
||||
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">
|
||||
<path
|
||||
|
@ -120,7 +122,11 @@ const thumbnail =
|
|||
data-pagefind-body={props.isDraft ? undefined : ""}
|
||||
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}
|
||||
</h1>
|
||||
<section
|
||||
|
@ -136,7 +142,7 @@ const thumbnail =
|
|||
{
|
||||
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")}
|
||||
{t(props.lang, "published_content/draft_warning")}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
|
@ -155,7 +161,7 @@ const thumbnail =
|
|||
<img
|
||||
loading="eager"
|
||||
src={thumbnail.src}
|
||||
alt={`Cover art for ${props.title}`}
|
||||
alt={t(props.lang, "published_content/cover_art_alt", props.title)}
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
|
@ -165,7 +171,7 @@ const thumbnail =
|
|||
) : null
|
||||
}
|
||||
<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>
|
||||
<slot />
|
||||
</Prose>
|
||||
|
@ -177,28 +183,26 @@ const thumbnail =
|
|||
id="draft-warning-bottom"
|
||||
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>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label="Publish date"
|
||||
aria-description={props.pubDate.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
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>
|
||||
) : null
|
||||
}
|
||||
<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">
|
||||
{t(props.lang, "story/description")}
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
|
@ -211,14 +215,50 @@ const thumbnail =
|
|||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span>{t(props.lang, "story/to_top")}</span></a
|
||||
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</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 ? (
|
||||
<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">
|
||||
Tags
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
|
@ -232,16 +272,12 @@ const thumbnail =
|
|||
</section>
|
||||
) : null
|
||||
}
|
||||
<MastodonComments
|
||||
instance={props.posts.mastodon?.instance}
|
||||
user={props.posts.mastodon?.user}
|
||||
postId={props.posts.mastodon?.postId}
|
||||
/>
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<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"
|
||||
>{t(props.lang, "story/licenses")}</a
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,21 +17,15 @@ import { formatCopyrightedCharacters } from "../utils/format_copyrighted_charact
|
|||
type Props = CollectionEntry<"stories">["data"];
|
||||
|
||||
const { props } = Astro;
|
||||
let prev = props.prev && (await getEntry(props.prev));
|
||||
if (prev && prev.data.isDraft) {
|
||||
prev = undefined;
|
||||
}
|
||||
let next = props.next && (await getEntry(props.next));
|
||||
if (next && next.data.isDraft) {
|
||||
next = undefined;
|
||||
}
|
||||
const prev = props.prev && (await getEntry(props.prev));
|
||||
const next = props.next && (await getEntry(props.next));
|
||||
const series = props.series && (await getEntry(props.series));
|
||||
const authorsList = await getEntries(props.authors);
|
||||
const commissionersList = props.commissioner && (await getEntries(props.commissioner));
|
||||
const requestersList = props.requester && (await getEntries(props.requester));
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
|
||||
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(
|
||||
(await getCollection("tag-categories")).flatMap((category) =>
|
||||
category.data.tags.map<[string, string | null]>(({ name }) =>
|
||||
|
@ -57,7 +51,7 @@ const thumbnail =
|
|||
const wordCount = props.wordCount?.toString();
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={props.title}>
|
||||
<BaseLayout pageTitle={props.title} lang={props.lang}>
|
||||
<Fragment slot="head">
|
||||
<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)} />
|
||||
|
@ -68,7 +62,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<meta content={thumbnail.src} property="og:image" data-pagefind-meta="image[content]" />
|
||||
<meta
|
||||
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]"
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -92,7 +86,7 @@ const wordCount = props.wordCount?.toString();
|
|||
href={series ? series.data.url : "/stories/1"}
|
||||
class="text-link my-1 h-9 w-9 p-2"
|
||||
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")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
|
@ -104,7 +98,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<a
|
||||
href="#description"
|
||||
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">
|
||||
<path
|
||||
|
@ -115,7 +109,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<button
|
||||
data-dark-mode
|
||||
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">
|
||||
<path
|
||||
|
@ -136,7 +130,7 @@ const wordCount = props.wordCount?.toString();
|
|||
data-pagefind-meta="type:story"
|
||||
>
|
||||
{
|
||||
prev || next ? (
|
||||
(prev && !prev.data.isDraft) || (next && !next.data.isDraft) ? (
|
||||
<div class="print:hidden">
|
||||
<div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
|
||||
{prev ? (
|
||||
|
@ -170,7 +164,11 @@ const wordCount = props.wordCount?.toString();
|
|||
</div>
|
||||
) : 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}
|
||||
</h1>
|
||||
<section
|
||||
|
@ -180,13 +178,6 @@ const wordCount = props.wordCount?.toString();
|
|||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
|
||||
</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 && (
|
||||
<Requesters lang={props.lang}>
|
||||
|
@ -205,6 +196,13 @@ const wordCount = props.wordCount?.toString();
|
|||
</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">
|
||||
<p>
|
||||
{t(props.lang, "story/warnings", wordCount, props.contentWarning)}
|
||||
|
@ -218,7 +216,7 @@ const wordCount = props.wordCount?.toString();
|
|||
<img
|
||||
loading="eager"
|
||||
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}
|
||||
height={props.thumbnailHeight}
|
||||
class="mx-auto my-5 shadow-lg"
|
||||
|
@ -227,7 +225,7 @@ const wordCount = props.wordCount?.toString();
|
|||
) : null
|
||||
}
|
||||
<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>
|
||||
<slot />
|
||||
</Prose>
|
||||
|
@ -239,28 +237,26 @@ const wordCount = props.wordCount?.toString();
|
|||
id="draft-warning-bottom"
|
||||
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>
|
||||
) : props.pubDate ? (
|
||||
<p
|
||||
id="publish-date"
|
||||
class="mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
|
||||
aria-label="Publish date"
|
||||
aria-description={props.pubDate.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
aria-label={t(props.lang, "published_content/publish_date_aria_label")}
|
||||
aria-description={
|
||||
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
|
||||
}
|
||||
data-pagefind-index-attrs="aria-description"
|
||||
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>
|
||||
) : null
|
||||
}
|
||||
<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">
|
||||
{t(props.lang, "story/description")}
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
|
@ -292,7 +288,7 @@ const wordCount = props.wordCount?.toString();
|
|||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span>{t(props.lang, "story/to_top")}</span></a
|
||||
><span>{t(props.lang, "published_content/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
|
@ -335,13 +331,31 @@ const wordCount = props.wordCount?.toString();
|
|||
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">
|
||||
Related stories
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{relatedStories.map((stories) => (
|
||||
{relatedStories.map((story) => (
|
||||
<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>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -353,7 +367,7 @@ const wordCount = props.wordCount?.toString();
|
|||
tags.length > 0 ? (
|
||||
<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">
|
||||
{t(props.lang, "story/tags")}
|
||||
{t(props.lang, "published_content/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{tags.map(({ id, name }) => (
|
||||
|
@ -367,16 +381,12 @@ const wordCount = props.wordCount?.toString();
|
|||
</section>
|
||||
) : null
|
||||
}
|
||||
<MastodonComments
|
||||
instance={props.posts.mastodon?.instance}
|
||||
user={props.posts.mastodon?.user}
|
||||
postId={props.posts.mastodon?.postId}
|
||||
/>
|
||||
{props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
|
||||
</main>
|
||||
<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"
|
||||
>{t(props.lang, "story/licenses")}</a
|
||||
>{t(props.lang, "published_content/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { APIRoute, GetStaticPaths } from "astro";
|
||||
import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
|
||||
import type { Website } from "../../../content/config";
|
||||
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
|
||||
import type { Lang, Website } from "../../../content/config";
|
||||
import { t } from "../../../i18n";
|
||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||
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 {
|
||||
const link = user.data.links[website];
|
||||
if (link) {
|
||||
if (typeof link === "string") {
|
||||
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(/^@/, "");
|
||||
}
|
||||
if (link?.username) {
|
||||
return link.username;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
case "eka":
|
||||
if ("eka" in user.data.links) {
|
||||
if ("eka" in links) {
|
||||
return `:icon${getUsernameForWebsite(user, "eka")}:`;
|
||||
}
|
||||
break;
|
||||
case "furaffinity":
|
||||
if ("furaffinity" in user.data.links) {
|
||||
if ("furaffinity" in links) {
|
||||
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
|
||||
}
|
||||
break;
|
||||
case "weasyl":
|
||||
if ("weasyl" in user.data.links) {
|
||||
if ("weasyl" in links) {
|
||||
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
||||
|
@ -123,7 +60,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
|||
}
|
||||
break;
|
||||
case "inkbunny":
|
||||
if ("inkbunny" in user.data.links) {
|
||||
if ("inkbunny" in links) {
|
||||
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
|
||||
|
@ -134,7 +71,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
|||
}
|
||||
break;
|
||||
case "sofurry":
|
||||
if ("sofurry" in user.data.links) {
|
||||
if ("sofurry" in links) {
|
||||
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
|
||||
|
@ -143,11 +80,12 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
|||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled ExportWebsite "${website}"`);
|
||||
const unknown: never = website;
|
||||
throw new Error(`Unhandled export website "${unknown}"`);
|
||||
}
|
||||
if (user.data.preferredLink) {
|
||||
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
|
||||
return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
|
||||
if (preferredLink) {
|
||||
const preferred = links[preferredLink]!;
|
||||
return `[${getUsernameForLang(user, lang)}](${preferred.link})`;
|
||||
}
|
||||
throw new Error(
|
||||
`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(
|
||||
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
||||
const u = (user: CollectionEntry<"users">) =>
|
||||
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website);
|
||||
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website, lang);
|
||||
const storyDescription = (
|
||||
[
|
||||
story.data.description,
|
||||
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
||||
t(
|
||||
lang,
|
||||
"export_story/writing",
|
||||
"export_story/authors",
|
||||
authorsList.map((author) => u(author)),
|
||||
),
|
||||
requestersList &&
|
||||
|
@ -216,12 +154,14 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||
);
|
||||
if (exportFormat === "bbcode") {
|
||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
||||
} else if (exportFormat === "markdown") {
|
||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
} else {
|
||||
throw new Error(`Unhandled export format "${exportFormat}"`);
|
||||
switch (exportFormat) {
|
||||
case "bbcode":
|
||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
||||
case "markdown":
|
||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
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")
|
||||
.trim();
|
||||
|
||||
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
story: storyText,
|
||||
description,
|
||||
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";
|
||||
|
||||
const content = { isAlive: true };
|
||||
|
||||
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
if (import.meta.env.PROD) {
|
||||
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 userName = getUsernameForLang(user, lang);
|
||||
if (user.data.preferredLink) {
|
||||
const link = user.data.links[user.data.preferredLink]!;
|
||||
return `<a href="${typeof link === "string" ? link : link[0]}">${userName}</a>`;
|
||||
return `<a href="${user.data.links[user.data.preferredLink]!.link}">${userName}</a>`;
|
||||
}
|
||||
return userName;
|
||||
};
|
||||
|
|
|
@ -18,17 +18,6 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
};
|
||||
|
||||
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();
|
||||
---
|
||||
|
||||
|
|
|
@ -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 GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
|
|
|
@ -20,21 +20,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
|
||||
const story = Astro.props;
|
||||
const readingTime = getReadingTime(story.body);
|
||||
if (!story.data.isDraft) {
|
||||
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) {
|
||||
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 150) {
|
||||
console.warn(
|
||||
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
|
||||
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
||||
|
|
|
@ -10,65 +10,53 @@ interface Tag {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
const [stories, games, tagCategories] = await Promise.all([
|
||||
const [stories, games, tagCategories, seriesCollection] = await Promise.all([
|
||||
getCollection("stories"),
|
||||
getCollection("games"),
|
||||
getCollection("tag-categories"),
|
||||
getCollection("series"),
|
||||
]);
|
||||
const tagsSet = new Set<string>();
|
||||
const uncategorizedTagsSet = new Set<string>();
|
||||
const draftOnlyTagsSet = new Set<string>();
|
||||
const seriesCollection = await getCollection("series");
|
||||
// Add tags from non-drafts to set; then, add tags only from drafts to separate set
|
||||
[stories, games]
|
||||
.flat()
|
||||
.sort((a, b) => (a.data.isDraft ? 1 : b.data.isDraft ? -1 : 0))
|
||||
.forEach((value) => {
|
||||
if (value.data.isDraft) {
|
||||
value.data.tags.forEach((tag) => {
|
||||
if (!tagsSet.has(tag)) {
|
||||
draftOnlyTagsSet.add(tag);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
value.data.tags.forEach((tag) => {
|
||||
tagsSet.add(tag);
|
||||
});
|
||||
}
|
||||
});
|
||||
[stories, games].flat().forEach(({ data: { isDraft, tags } }) => {
|
||||
if (isDraft) {
|
||||
tags.forEach((tag) => draftOnlyTagsSet.add(tag));
|
||||
} else {
|
||||
tags.forEach((tag) => uncategorizedTagsSet.add(tag));
|
||||
}
|
||||
});
|
||||
// Tags from published content shouldn't be included in drafts-only set
|
||||
uncategorizedTagsSet.forEach((tag) => draftOnlyTagsSet.delete(tag));
|
||||
|
||||
const uncategorizedTagsSet = new Set(tagsSet);
|
||||
const uniqueSlugs = new Set<string>();
|
||||
const categorizedTags = tagCategories
|
||||
.sort((a, b) => {
|
||||
if (a.data.index == b.data.index) {
|
||||
throw new Error(
|
||||
`Found tag categories with same index value ${a.data.index} ("${a.data.name}", "${b.data.name}")`,
|
||||
);
|
||||
throw new Error(`Found tag categories with same index value ${a.data.index} ("${a.id}", "${b.id}")`);
|
||||
}
|
||||
return a.data.index - b.data.index;
|
||||
})
|
||||
.map((category) => {
|
||||
const tagList = category.data.tags.map<Tag>(({ name, description }) => {
|
||||
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ");
|
||||
const tag = typeof name === "string" ? name : name["eng"];
|
||||
const id = slug(tag);
|
||||
return { id, name: tag, description };
|
||||
const tag = typeof name === "string" ? name : name.en;
|
||||
return { id: slug(tag), name: tag, description };
|
||||
});
|
||||
tagList.forEach(({ name }, index) => {
|
||||
if (index !== tagList.findLastIndex(({ name: otherTag }) => name == otherTag)) {
|
||||
throw new Error(`Duplicated tag "${name}" found in multiple tag-categories lists`);
|
||||
tagList.forEach(({ id, name }) => {
|
||||
if (uniqueSlugs.has(id)) {
|
||||
throw new Error(`Duplicated tag "${name}" found in multiple tag-categories entries`);
|
||||
}
|
||||
uniqueSlugs.add(id);
|
||||
});
|
||||
return {
|
||||
name: category.data.name,
|
||||
id: category.id,
|
||||
id: slug(category.data.name),
|
||||
tags: tagList.filter(({ name }) => {
|
||||
if (draftOnlyTagsSet.has(name)) {
|
||||
console.log(`Omitting draft-only tag "${name}"`);
|
||||
// console.log(`Omitting draft-only tag "${name}"`);
|
||||
return false;
|
||||
}
|
||||
if (tagsSet.has(name)) {
|
||||
uncategorizedTagsSet.delete(name);
|
||||
}
|
||||
uncategorizedTagsSet.delete(name);
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
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 { slug } from "github-slugger";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
|
@ -45,24 +45,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
const tagDescriptions = tagCategories.reduce(
|
||||
(acc, category) => {
|
||||
category.data.tags.forEach(({ name, description, related }) => {
|
||||
if (related) {
|
||||
related = related.filter((relatedTag) => {
|
||||
if (relatedTag == name) {
|
||||
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
||||
return false;
|
||||
}
|
||||
if (!tags.has(relatedTag)) {
|
||||
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
const key = typeof name === "string" ? name : name["eng"];
|
||||
related = related.filter((relatedTag) => {
|
||||
if (relatedTag == name) {
|
||||
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
||||
return false;
|
||||
}
|
||||
if (!tags.has(relatedTag)) {
|
||||
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const key = typeof name === "string" ? name : name.en;
|
||||
if (key in acc) {
|
||||
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;
|
||||
},
|
||||
|
|
|
@ -2,14 +2,15 @@ import type { CollectionEntry } from "astro:content";
|
|||
import { DEFAULT_LANG, type Lang } from "../content/config";
|
||||
|
||||
export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang): string {
|
||||
if (user.data.lang) {
|
||||
if (user.data.lang[lang]) {
|
||||
return user.data.lang[lang];
|
||||
const { name } = user.data;
|
||||
if (typeof name === "object") {
|
||||
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) {
|
||||
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 { 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
Reference in a new issue