Much changes

This commit is contained in:
DrMint 2023-08-11 23:11:10 +02:00
parent 9b6e68113c
commit d9f84f59a8
52 changed files with 2363 additions and 396 deletions

View File

@ -1,4 +1,13 @@
MONGODB_URI=mongodb://mongo:27017/payload MONGODB_URI=mongodb://mongo:27017/payload
MONGODB_PORT=27017 MONGODB_PORT=27017
PAYLOAD_URI=https://payload.domain.com
PAYLOAD_SECRET=payloadsecreta5e6ea45ef4e66eaa151612bdcb599df PAYLOAD_SECRET=payloadsecreta5e6ea45ef4e66eaa151612bdcb599df
PAYLOAD_PORT=3000 PAYLOAD_PORT=3000
STRAPI_URI=https://strapi.domain.com
STRAPI_TOKEN=strapisecreta5e6ea45ef4e66eaa151612bdcb599df
SEEDING_ADMIN_USERNAME=admin_name
SEEDING_ADMIN_EMAIL=email@domain.com
SEEDING_ADMIN_PASSWORD=somepassword

View File

@ -2,4 +2,8 @@
"css.lint.unknownAtRules": "ignore", "css.lint.unknownAtRules": "ignore",
"editor.rulers": [100], "editor.rulers": [100],
"typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifier": "non-relative",
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
}
} }

250
package-lock.json generated
View File

@ -9,24 +9,29 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/vollkorn": "^5.0.5", "@fontsource/vollkorn": "^5.0.8",
"clean-deep": "^3.4.0", "clean-deep": "^3.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"iso-639-1": "^2.1.15", "language-tags": "^1.0.8",
"payload": "^1.11.7", "luxon": "^3.4.0",
"payload": "^1.13.3",
"qs": "^6.11.2",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"styled-components": "^6.0.5", "styled-components": "^6.0.7",
"unset-value": "^2.0.1" "unset-value": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/language-tags": "^1.0.1",
"@types/luxon": "^3.3.1",
"@types/qs": "^6.9.7",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^3.0.0", "prettier": "^3.0.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.6" "typescript": "^5.1.6"
} }
@ -3687,9 +3692,9 @@
} }
}, },
"node_modules/@fontsource/vollkorn": { "node_modules/@fontsource/vollkorn": {
"version": "5.0.5", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/vollkorn/-/vollkorn-5.0.5.tgz", "resolved": "https://registry.npmjs.org/@fontsource/vollkorn/-/vollkorn-5.0.8.tgz",
"integrity": "sha512-8YQT9VLc6z0rxuyrDRLrcQnYFJ8eZOOKs4Oat7HUMHYAwRe20XK1khUkiTLb5jGXD4XDfAwFW9sV21SJoyUhHQ==" "integrity": "sha512-QSPfmwObfsqSNMJlDWeixrycNiOtTh8VrCP1khT1u3wUhESHAgj+FVcyB9IdWj9Z1jTWc1fZ9atPunSovze0YA=="
}, },
"node_modules/@hapi/hoek": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
@ -4375,9 +4380,9 @@
} }
}, },
"node_modules/@swc/core": { "node_modules/@swc/core": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.75.tgz",
"integrity": "sha512-njGQuJO+Wy06dEayt70cf0c/KI3HGjm4iW9LLViVLBuYNzJ4SSdNfzejludzufu6im+dsDJ0i3QjgWhAIcVHMQ==", "integrity": "sha512-YLqd5oZVnaOq/OzkjRSsJUQqAfKYiD0fzUyVUPVlNNCoQEfVfSMcXH80hLmYe9aDH0T/a7qEMjWyIr/0kWqy1A==",
"hasInstallScript": true, "hasInstallScript": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -4387,16 +4392,16 @@
"url": "https://opencollective.com/swc" "url": "https://opencollective.com/swc"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-darwin-arm64": "1.3.68", "@swc/core-darwin-arm64": "1.3.75",
"@swc/core-darwin-x64": "1.3.68", "@swc/core-darwin-x64": "1.3.75",
"@swc/core-linux-arm-gnueabihf": "1.3.68", "@swc/core-linux-arm-gnueabihf": "1.3.75",
"@swc/core-linux-arm64-gnu": "1.3.68", "@swc/core-linux-arm64-gnu": "1.3.75",
"@swc/core-linux-arm64-musl": "1.3.68", "@swc/core-linux-arm64-musl": "1.3.75",
"@swc/core-linux-x64-gnu": "1.3.68", "@swc/core-linux-x64-gnu": "1.3.75",
"@swc/core-linux-x64-musl": "1.3.68", "@swc/core-linux-x64-musl": "1.3.75",
"@swc/core-win32-arm64-msvc": "1.3.68", "@swc/core-win32-arm64-msvc": "1.3.75",
"@swc/core-win32-ia32-msvc": "1.3.68", "@swc/core-win32-ia32-msvc": "1.3.75",
"@swc/core-win32-x64-msvc": "1.3.68" "@swc/core-win32-x64-msvc": "1.3.75"
}, },
"peerDependencies": { "peerDependencies": {
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
@ -4408,9 +4413,9 @@
} }
}, },
"node_modules/@swc/core-darwin-arm64": { "node_modules/@swc/core-darwin-arm64": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.75.tgz",
"integrity": "sha512-Z5pNxeuP2NxpOHTzDQkJs0wAPLnTlglZnR3WjObijwvdwT/kw1Y5EPDKM/BVSIeG40SPMkDLBbI0aj0qyXzrBA==", "integrity": "sha512-anDnx9L465lGbjB2mvcV54NGHW6illr0IDvVV7JmkabYUVneaRdQvTr0tbHv3xjHnjrK1wuwVOHKV0LcQF2tnQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4423,9 +4428,9 @@
} }
}, },
"node_modules/@swc/core-darwin-x64": { "node_modules/@swc/core-darwin-x64": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.75.tgz",
"integrity": "sha512-ZHl42g6yXhfX4PzAQ0BNvBXpt/OcbAHfubWRN6eXELK3fiNnxL7QBW1if7iizlq6iA+Mj1pwHyyUit1pz0+fgA==", "integrity": "sha512-dIHDfrLmeZfr2xwi1whO7AmzdI3HdamgvxthaL+S8L1x8TeczAZEvsmZTjy3s8p3Va4rbGXcb3+uBhmfkqCbfw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4438,9 +4443,9 @@
} }
}, },
"node_modules/@swc/core-linux-arm-gnueabihf": { "node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.75.tgz",
"integrity": "sha512-Mk8f6KCOQ2CNAR4PtWajIjS6XKSSR7ZYDOCf1GXRxhS3qEyQH7V8elWvqWYqHcT4foO60NUmxA/NOM/dQrdO1A==", "integrity": "sha512-qeJmvMGrjC6xt+G0R4kVqqxvlhxJx7tTzhcEoWgLJnfvGZiF6SJdsef4OSM7HuReXrlBoEtJbfGPrLJtbV+C0w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -4453,9 +4458,9 @@
} }
}, },
"node_modules/@swc/core-linux-arm64-gnu": { "node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.75.tgz",
"integrity": "sha512-RhBllggh9t9sIxaRgRcGrVaS7fDk6KsIqR6b9+dwU5OyDr4ZyHWw1ZaH/1/HAebuXYhNBjoNUiRtca6lKRIPgQ==", "integrity": "sha512-sqA9JqHEJBF4AdNuwo5zRqq0HC3l31SPsG9zpRa4nRzG5daBBJ80H7fi6PZQud1rfNNq+Q08gjYrdrxwHstvjw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4468,9 +4473,9 @@
} }
}, },
"node_modules/@swc/core-linux-arm64-musl": { "node_modules/@swc/core-linux-arm64-musl": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.75.tgz",
"integrity": "sha512-8K3zjU+tFgn6yGDEeD343gkKaHU9dhz77NiVkI1VzwRaT/Ag5pwl5eMQ1yStm8koNFzn3zq6rGjHfI5g2yI5Wg==", "integrity": "sha512-95rQT5xTAL3eKhMJbJbLsZHHP9EUlh1rcrFoLf0gUApoVF8g94QjZ9hYZiI72mMP5WPjgTEXQVnVB9O2GxeaLw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4483,9 +4488,9 @@
} }
}, },
"node_modules/@swc/core-linux-x64-gnu": { "node_modules/@swc/core-linux-x64-gnu": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.75.tgz",
"integrity": "sha512-4xAnvsBOyeTL0AB8GWlRKDM/hsysJ5jr5qvdKKI3rZfJgnnxl/xSX6TJKPsJ8gygfUJ3BmfCbmUmEyeDZ3YPvA==", "integrity": "sha512-If7UpAhnPduMmtC+TSgPpZ1UXZfp2hIpjUFxpeCmHHYLS6Fn/2GZC5hpEiu+wvFJF0hzPh93eNAHa9gUxGUG+w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4498,9 +4503,9 @@
} }
}, },
"node_modules/@swc/core-linux-x64-musl": { "node_modules/@swc/core-linux-x64-musl": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.75.tgz",
"integrity": "sha512-RCpaBo1fcpy1EFdjF+I7N4lfzOaHXVV0iMw/ABM+0PD6tp3V/9pxsguaZyeAHyEiUlDA6PZ4TfXv5zfnXEgW4Q==", "integrity": "sha512-HOhxX0YNHTElCZqIviquka3CGYTN8rSQ6BdFfSk/K0O+ZEHx3qGte0qr+gGLPF/237GxreUkp3OMaWKuURtuCg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4513,9 +4518,9 @@
} }
}, },
"node_modules/@swc/core-win32-arm64-msvc": { "node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.75.tgz",
"integrity": "sha512-v2WZvXrSslYEpY1nqpItyamL4DyaJinmOkXvM8Bc1LLKU5rGuvmBdjUYg/5Y+o0AUynuiWubpgHNOkBWiCvfqw==", "integrity": "sha512-7QPI+mvBXAerVfWahrgBNe+g7fK8PuetxFnZSEmXUcDXvWcdJXAndD7GjAJzbDyjQpLKHbsDKMiHYvfNxZoN/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4528,9 +4533,9 @@
} }
}, },
"node_modules/@swc/core-win32-ia32-msvc": { "node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.75.tgz",
"integrity": "sha512-HH5NJrIdzkJs+1xxprie0qSCMBeL9yeEhcC1yZTzYv8bwmabOUSdtKIqS55iYP/2hLWn9CTbvKPmLOIhCopW3Q==", "integrity": "sha512-EfABCy4Wlq7O5ShWsm32FgDkSjyeyj/SQ4wnUIvWpkXhgfT1iNXky7KRU1HtX+SmnVk/k/NnabVZpIklYbjtZA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -4543,9 +4548,9 @@
} }
}, },
"node_modules/@swc/core-win32-x64-msvc": { "node_modules/@swc/core-win32-x64-msvc": {
"version": "1.3.68", "version": "1.3.75",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.68.tgz", "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.75.tgz",
"integrity": "sha512-9HZVtLQUgK8r/yXQdwe0VBexbIcrY6+fBROhs7AAPWdewpaUeLkwQEJk6TbYr9CQuHw26FFGg6SjwAiqXF+kgQ==", "integrity": "sha512-cTvP0pOD9C3pSp1cwtt85ZsrUkQz8RZfSPhM+jCGxKxmoowDCnInoOQ4Ica/ehyuUnQ4/IstSdYtYpO5yzPDJg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4743,11 +4748,23 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA=="
}, },
"node_modules/@types/language-tags": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/language-tags/-/language-tags-1.0.1.tgz",
"integrity": "sha512-rTtRNIewaBrkMUfsCe7ES3xsTRQcEVgic2yoDY9hM3D/nwmABcG2du4l4+dTbWvfO8pUYwL4/2TbWFJa/AGc2g==",
"dev": true
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.14.195", "version": "4.14.195",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg=="
}, },
"node_modules/@types/luxon": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz",
"integrity": "sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==",
"dev": true
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -5474,6 +5491,20 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/body-parser/node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/body-scroll-lock": { "node_modules/body-scroll-lock": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz",
@ -6034,6 +6065,14 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/console-table-printer": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.11.2.tgz",
"integrity": "sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==",
"dependencies": {
"simple-wcswidth": "^1.0.1"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -7192,6 +7231,20 @@
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz",
"integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==" "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="
}, },
"node_modules/express/node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ext": { "node_modules/ext": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
@ -8476,14 +8529,6 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
}, },
"node_modules/iso-639-1": {
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.15.tgz",
"integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/isobject": { "node_modules/isobject": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
@ -8760,6 +8805,19 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/language-subtag-registry": {
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
"integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w=="
},
"node_modules/language-tags": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.8.tgz",
"integrity": "sha512-aWAZwgPLS8hJ20lNPm9HNVs4inexz6S2sQa3wx/+ycuutMNE5/IfYxiWYBbi+9UWCQVaXYCOPUl6gFrPR7+jGg==",
"dependencies": {
"language-subtag-registry": "^0.3.20"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -8888,6 +8946,14 @@
"es5-ext": "~0.10.2" "es5-ext": "~0.10.2"
} }
}, },
"node_modules/luxon": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.0.tgz",
"integrity": "sha512-7eDo4Pt7aGhoCheGFIuq4Xa2fJm4ZpmldpGhjTYBNUYNCN6TIEP6v7chwwwt3KRp7YR+rghbfvjyo3V5y9hgBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@ -9251,23 +9317,6 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": { "node_modules/napi-build-utils": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@ -9848,9 +9897,9 @@
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
}, },
"node_modules/payload": { "node_modules/payload": {
"version": "1.11.7", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/payload/-/payload-1.11.7.tgz", "resolved": "https://registry.npmjs.org/payload/-/payload-1.13.4.tgz",
"integrity": "sha512-pVImP0b8MA9VfcXDITqq8KlIVaFB81FMtt24TPrBfNqkG53cRFxnYiGRJS40p/CNVknCpSRGVBkdpKmVNQ07YQ==", "integrity": "sha512-toVqxxxq4SJwBJtmCLaLOReza0JSidCrH4jTBxcy4h/qOYyeCAS4wApUFYhDI7JhB3KCd2+jbpVbhHOU4ALBKA==",
"dependencies": { "dependencies": {
"@date-io/date-fns": "^2.16.0", "@date-io/date-fns": "^2.16.0",
"@dnd-kit/core": "^6.0.7", "@dnd-kit/core": "^6.0.7",
@ -9859,7 +9908,7 @@
"@faceless-ui/scroll-info": "^1.3.0", "@faceless-ui/scroll-info": "^1.3.0",
"@faceless-ui/window-info": "^2.1.1", "@faceless-ui/window-info": "^2.1.1",
"@monaco-editor/react": "^4.5.1", "@monaco-editor/react": "^4.5.1",
"@swc/core": "^1.3.26", "@swc/core": "1.3.75",
"@swc/register": "^0.1.10", "@swc/register": "^0.1.10",
"@types/sharp": "^0.31.1", "@types/sharp": "^0.31.1",
"body-parser": "^1.20.1", "body-parser": "^1.20.1",
@ -9867,6 +9916,7 @@
"compression": "^1.7.4", "compression": "^1.7.4",
"conf": "^10.2.0", "conf": "^10.2.0",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"console-table-printer": "^2.11.2",
"css-loader": "^5.2.7", "css-loader": "^5.2.7",
"css-minimizer-webpack-plugin": "^5.0.0", "css-minimizer-webpack-plugin": "^5.0.0",
"dataloader": "^2.1.0", "dataloader": "^2.1.0",
@ -11386,6 +11436,23 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
}, },
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/prebuild-install": { "node_modules/prebuild-install": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@ -11412,9 +11479,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
@ -11510,9 +11577,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
}, },
@ -12488,6 +12555,11 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/simple-wcswidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz",
"integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg=="
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "1.0.19", "version": "1.0.19",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz",
@ -12782,9 +12854,9 @@
} }
}, },
"node_modules/styled-components": { "node_modules/styled-components": {
"version": "6.0.5", "version": "6.0.7",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.0.5.tgz", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.0.7.tgz",
"integrity": "sha512-308zi5o7LrA9cVaP4nPD0TaUpOjGPePkAUFb/OGB0xRI3I9ozpW5UyASvRVi9wJcYASG+Y3mLDLDUZC7nqzimw==", "integrity": "sha512-xIwWuiRMYR43mskVsW9MGTRjSo7ol4bcVjT595fGUp3OLBJOlOgaiKaxsHdC4a2HqWKqKnh0CmcRbk5ogyDjTg==",
"dependencies": { "dependencies": {
"@babel/cli": "^7.21.0", "@babel/cli": "^7.21.0",
"@babel/core": "^7.21.0", "@babel/core": "^7.21.0",

View File

@ -16,27 +16,34 @@
"prettier": "prettier --list-different --end-of-line auto --write src", "prettier": "prettier --list-different --end-of-line auto --write src",
"tsc": "tsc --noEmit", "tsc": "tsc --noEmit",
"precommit": "npm run generate:types && npm run prettier && npm run tsc", "precommit": "npm run generate:types && npm run prettier && npm run tsc",
"upgrade": "ncu" "upgrade": "ncu",
"clean": "sudo rm -r uploads mongo",
"start": "sudo docker compose up"
}, },
"dependencies": { "dependencies": {
"@fontsource/vollkorn": "^5.0.5", "@fontsource/vollkorn": "^5.0.8",
"clean-deep": "^3.4.0", "clean-deep": "^3.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"iso-639-1": "^2.1.15", "language-tags": "^1.0.8",
"payload": "^1.11.7", "luxon": "^3.4.0",
"payload": "^1.13.3",
"qs": "^6.11.2",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"styled-components": "^6.0.5", "styled-components": "^6.0.7",
"unset-value": "^2.0.1" "unset-value": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/language-tags": "^1.0.1",
"@types/luxon": "^3.3.1",
"@types/qs": "^6.9.7",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^3.0.0", "prettier": "^3.0.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.6" "typescript": "^5.1.6"
} }

View File

@ -0,0 +1 @@
export const publicAccess = () => true;

View File

@ -0,0 +1,70 @@
import { CollectionConfig } from "payload/types";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroups, Collections } from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
slug: "slug",
startingYear: "startingYear",
endingYear: "endingYear",
translations: "translations",
translationsTitle: "title",
translationsDescription: "description",
} as const satisfies Record<string, string>;
export const ChronologyEras: CollectionConfig = buildCollectionConfig(
Collections.ChronologyEras,
{
singular: "Chronology Era",
plural: "Chronology Eras",
},
() => ({
defaultSort: fields.startingYear,
admin: {
group: CollectionGroups.Collections,
defaultColumns: [fields.slug, fields.startingYear, fields.endingYear, fields.translations],
useAsTitle: fields.slug,
},
access: {
create: mustBeAdmin,
delete: mustBeAdmin,
},
endpoints: [importFromStrapi],
fields: [
slugField({ name: fields.slug }),
{
type: "row",
fields: [
{
name: fields.startingYear,
type: "number",
min: 0,
required: true,
admin: { width: "50%", description: "The year the era started (year included)" },
},
{
name: fields.endingYear,
type: "number",
min: 0,
required: true,
admin: { width: "50%", description: "The year the era ended (year included)" },
},
],
},
localizedFields({
name: fields.translations,
admin: { useAsTitle: fields.translationsTitle },
fields: [
{ name: fields.translationsTitle, type: "text", required: true },
{
name: fields.translationsDescription,
type: "textarea",
},
],
}),
],
})
);

View File

@ -0,0 +1,25 @@
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { ChronologyEra } from "../../../types/collections";
export const importFromStrapi = createStrapiImportEndpoint<ChronologyEra>({
strapi: {
collection: "chronology-eras",
params: {
populate: { title: { populate: "language" } },
},
},
payload: {
collection: Collections.ChronologyEras,
convert: ({ slug, starting_year, ending_year, title: titles }) => ({
slug,
startingYear: starting_year,
endingYear: ending_year,
translations: titles.map(({ language, title, description }) => ({
language: language.data.attributes.code,
title,
description,
})),
}),
},
});

View File

@ -0,0 +1,120 @@
import { DateTime } from "luxon";
import { CollectionConfig } from "payload/types";
import {
QuickFilters,
languageBasedFilters,
publishStatusFilters,
} from "../../components/QuickFilters";
import { CollectionGroups, Collections } from "../../constants";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { isDefined, isUndefined } from "../../utils/asserts";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
name: "name",
events: "events",
eventsTranslations: "translations",
eventsTranslationsTitle: "title",
eventsTranslationsDescription: "description",
eventsTranslationsNotes: "notes",
date: "date",
year: "year",
month: "month",
day: "day",
status: "_status",
} as const satisfies Record<string, string>;
export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig(
Collections.ChronologyItems,
{
singular: "Chronology Item",
plural: "Chronology Items",
},
() => ({
defaultSort: fields.name,
admin: {
group: CollectionGroups.Collections,
defaultColumns: [fields.name, fields.events, fields.status],
useAsTitle: fields.name,
components: {
BeforeListTable: [
() =>
QuickFilters({
slug: Collections.ChronologyItems,
filterGroups: [
languageBasedFilters("events.translations.language"),
publishStatusFilters,
],
}),
],
},
},
endpoints: [importFromStrapi],
fields: [
{
name: fields.name,
type: "text",
admin: { hidden: true },
hooks: {
beforeValidate: [
({
data: {
date: { year, month, day },
},
}) =>
[
String(year ?? "?????").padStart(5, "0"),
String(month ?? "??").padStart(2, "0"),
String(day ?? "??").padStart(2, "0"),
].join("-"),
],
},
},
{
type: "group",
name: fields.date,
validate: ({ year, month, day } = {}) => {
if (isDefined(day)) {
if (isUndefined(month)) return "A month is required if a day is set";
const stringDate = `${year}/${month}/${day}`;
if (!DateTime.fromObject({ year, month, day }).isValid)
return `The given date (${stringDate}) is not a valid date.`;
}
return true;
},
fields: [
{
type: "row",
fields: [
{
name: fields.year,
type: "number",
required: true,
min: 0,
admin: { width: "33%" },
},
{ name: fields.month, type: "number", min: 1, max: 12, admin: { width: "33%" } },
{ name: fields.day, type: "number", min: 1, max: 31, admin: { width: "33%" } },
],
},
],
},
{
name: fields.events,
type: "array",
fields: [
localizedFields({
name: fields.eventsTranslations,
admin: { useAsTitle: fields.eventsTranslationsTitle },
fields: [
{ name: fields.eventsTranslationsTitle, type: "text" },
{ name: fields.eventsTranslationsDescription, type: "textarea" },
{ name: fields.eventsTranslationsNotes, type: "textarea" },
],
}),
],
},
],
})
);

View File

@ -0,0 +1,26 @@
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { ChronologyItem } from "../../../types/collections";
export const importFromStrapi = createStrapiImportEndpoint<ChronologyItem>({
strapi: {
collection: "chronology-items",
params: {
populate: { events: { populate: { translations: { populate: "language" } } } },
},
},
payload: {
collection: Collections.ChronologyItems,
convert: ({ year, month, day, events }) => ({
date: { year, month, day },
events: events.map((event) => ({
translations: event.translations.map(({ title, description, note, language }) => ({
title,
description,
note,
language: language.data.attributes.code,
})),
})),
}),
},
});

View File

@ -1,17 +1,14 @@
import { CollectionGroup, FileTypes, KeysTypes } from "../../constants"; import { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { imageField } from "../../fields/imageField/imageField";
import { Keys } from "../Keys/Keys";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { Recorders } from "../Recorders/Recorders";
import { isDefined } from "../../utils/asserts";
import { fileField } from "../../fields/fileField/fileField"; import { fileField } from "../../fields/fileField/fileField";
import { contentBlocks } from "./Blocks/blocks"; import { imageField } from "../../fields/imageField/imageField";
import { ContentThumbnails } from "../ContentThumbnails/ContentThumbnails"; import { slugField } from "../../fields/slugField/slugField";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { isDefined } from "../../utils/asserts";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { contentBlocks } from "./Blocks/blocks";
const fields = { const fields = {
slug: "slug", slug: "slug",
@ -37,6 +34,7 @@ const fields = {
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Contents = buildVersionedCollectionConfig( export const Contents = buildVersionedCollectionConfig(
Collections.Contents,
{ {
singular: "Content", singular: "Content",
plural: "Contents", plural: "Contents",
@ -55,7 +53,7 @@ export const Contents = buildVersionedCollectionConfig(
fields.translations, fields.translations,
fields.status, fields.status,
], ],
group: CollectionGroup.Collections, group: CollectionGroups.Collections,
hooks: { hooks: {
beforeDuplicate: beforeDuplicatePiping([ beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish, beforeDuplicateUnpublish,
@ -71,7 +69,7 @@ export const Contents = buildVersionedCollectionConfig(
slugField({ name: fields.slug, admin: { width: "50%" } }), slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({ imageField({
name: fields.thumbnail, name: fields.thumbnail,
relationTo: ContentThumbnails.slug, relationTo: Collections.ContentsThumbnails,
admin: { width: "50%" }, admin: { width: "50%" },
}), }),
], ],
@ -82,7 +80,7 @@ export const Contents = buildVersionedCollectionConfig(
{ {
name: fields.categories, name: fields.categories,
type: "relationship", type: "relationship",
relationTo: [Keys.slug], relationTo: [Collections.Keys],
filterOptions: { type: { equals: KeysTypes.Categories } }, filterOptions: { type: { equals: KeysTypes.Categories } },
hasMany: true, hasMany: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
@ -90,7 +88,7 @@ export const Contents = buildVersionedCollectionConfig(
{ {
name: fields.type, name: fields.type,
type: "relationship", type: "relationship",
relationTo: [Keys.slug], relationTo: [Collections.Keys],
filterOptions: { type: { equals: KeysTypes.Contents } }, filterOptions: { type: { equals: KeysTypes.Contents } },
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
}, },
@ -128,7 +126,7 @@ export const Contents = buildVersionedCollectionConfig(
name: fields.textTranscribers, name: fields.textTranscribers,
label: "Transcribers", label: "Transcribers",
type: "relationship", type: "relationship",
relationTo: Recorders.slug, relationTo: Collections.Recorders,
hasMany: true, hasMany: true,
admin: { admin: {
condition: (_, siblingData) => condition: (_, siblingData) =>
@ -140,7 +138,7 @@ export const Contents = buildVersionedCollectionConfig(
name: fields.textTranslators, name: fields.textTranslators,
label: "Translators", label: "Translators",
type: "relationship", type: "relationship",
relationTo: Recorders.slug, relationTo: Collections.Recorders,
hasMany: true, hasMany: true,
admin: { admin: {
condition: (_, siblingData) => condition: (_, siblingData) =>
@ -152,7 +150,7 @@ export const Contents = buildVersionedCollectionConfig(
name: fields.textProofreaders, name: fields.textProofreaders,
label: "Proofreaders", label: "Proofreaders",
type: "relationship", type: "relationship",
relationTo: Recorders.slug, relationTo: Collections.Recorders,
hasMany: true, hasMany: true,
admin: { width: "50%" }, admin: { width: "50%" },
}, },

View File

@ -1,7 +1,6 @@
import { CollectionGroups, Collections } from "../../constants";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { CollectionGroup } from "../../constants";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { Contents } from "../Contents/Contents";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -12,18 +11,19 @@ const fields = {
contents: "contents", contents: "contents",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const ContentFolders = buildCollectionConfig( export const ContentsFolders = buildCollectionConfig(
Collections.ContentsFolders,
{ {
singular: "Content Folder", singular: "Contents Folder",
plural: "Content Folders", plural: "Contents Folders",
}, },
({ slug }) => ({ () => ({
defaultSort: fields.slug, defaultSort: fields.slug,
admin: { admin: {
useAsTitle: fields.slug, useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.translations], defaultColumns: [fields.slug, fields.translations],
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Collections, group: CollectionGroups.Collections,
}, },
timestamps: false, timestamps: false,
versions: false, versions: false,
@ -43,14 +43,14 @@ export const ContentFolders = buildCollectionConfig(
{ {
type: "relationship", type: "relationship",
name: fields.subfolders, name: fields.subfolders,
relationTo: [slug], relationTo: Collections.ContentsFolders,
hasMany: true, hasMany: true,
admin: { width: "50%" }, admin: { width: "50%" },
}, },
{ {
type: "relationship", type: "relationship",
name: fields.contents, name: fields.contents,
relationTo: [Contents.slug], relationTo: Collections.Contents,
hasMany: true, hasMany: true,
admin: { width: "50%" }, admin: { width: "50%" },
}, },

View File

@ -1,4 +1,4 @@
import { CollectionGroup } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -7,17 +7,18 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const ContentThumbnails = buildCollectionConfig( export const ContentsThumbnails = buildCollectionConfig(
Collections.ContentsThumbnails,
{ {
singular: "Content Thumbnail", singular: "Contents Thumbnail",
plural: "Content Thumbnails", plural: "Contents Thumbnails",
}, },
({ uploadDir }) => ({ ({ uploadDir }) => ({
defaultSort: fields.filename, defaultSort: fields.filename,
admin: { admin: {
useAsTitle: fields.filename, useAsTitle: fields.filename,
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Media, group: CollectionGroups.Media,
}, },
upload: { upload: {
staticDir: uploadDir, staticDir: uploadDir,

View File

@ -1,12 +1,15 @@
import { text } from "payload/dist/fields/validations";
import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroup } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = { const fields = {
id: "id", id: "id",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Currencies = buildCollectionConfig( export const Currencies = buildCollectionConfig(
Collections.Currencies,
{ {
singular: "Currency", singular: "Currency",
plural: "Currencies", plural: "Currencies",
@ -14,12 +17,14 @@ export const Currencies = buildCollectionConfig(
() => ({ () => ({
defaultSort: fields.id, defaultSort: fields.id,
admin: { admin: {
pagination: { defaultLimit: 100 },
useAsTitle: fields.id, useAsTitle: fields.id,
defaultColumns: [fields.id], defaultColumns: [fields.id],
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Meta, group: CollectionGroups.Meta,
}, },
access: { create: mustBeAdmin, update: mustBeAdmin }, access: { create: mustBeAdmin, update: mustBeAdmin },
endpoints: [importFromStrapi],
timestamps: false, timestamps: false,
fields: [ fields: [
{ {
@ -27,11 +32,11 @@ export const Currencies = buildCollectionConfig(
type: "text", type: "text",
unique: true, unique: true,
required: true, required: true,
validate: (value) => { validate: (value, options) => {
if (/^[A-Z]{3}$/g.test(value)) { if (!/^[A-Z]{3}$/g.test(value)) {
return true;
}
return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)"; return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)";
}
return text(value, options);
}, },
}, },
], ],

View File

@ -0,0 +1,14 @@
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { Language } from "../../../types/collections";
export const importFromStrapi = createStrapiImportEndpoint<Language>({
strapi: {
collection: "currencies",
params: {},
},
payload: {
collection: Collections.Currencies,
convert: ({ code, name }) => ({ id: code, name }),
},
});

View File

@ -1,4 +1,4 @@
import { CollectionGroup, FileTypes } from "../../constants"; import { CollectionGroups, Collections, FileTypes } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -7,6 +7,7 @@ const fields = {
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Files = buildCollectionConfig( export const Files = buildCollectionConfig(
Collections.Files,
{ {
singular: "File", singular: "File",
plural: "Files", plural: "Files",
@ -16,7 +17,7 @@ export const Files = buildCollectionConfig(
admin: { admin: {
useAsTitle: fields.filename, useAsTitle: fields.filename,
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Media, group: CollectionGroups.Media,
}, },
fields: [ fields: [
{ {

View File

@ -1,60 +1,86 @@
import { CollectionConfig } from "payload/types"; import payload from "payload";
import { slugField } from "../../fields/slugField/slugField"; import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroup, KeysTypes } from "../../constants"; import { QuickFilters } from "../../components/QuickFilters";
import { CollectionGroups, Collections, KeysTypes, LanguageCodes } from "../../constants";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { Key } from "../../types/collections"; import { Key } from "../../types/collections";
import { isDefined } from "../../utils/asserts"; import { isDefined } from "../../utils/asserts";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { importFromStrapi } from "./endpoints/importFromStrapi";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { QuickFilters } from "../../components/QuickFilters";
const fields = { const fields = {
slug: "slug",
translations: "translations",
type: "type",
name: "name", name: "name",
short: "short", type: "type",
translations: "translations",
translationsName: "name",
translationsShort: "short",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"]; const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"];
export const Keys: CollectionConfig = buildCollectionConfig( export const Keys = buildCollectionConfig(
Collections.Keys,
{ {
singular: "Key", singular: "Key",
plural: "Keys", plural: "Keys",
}, },
() => ({ () => ({
defaultSort: fields.slug, defaultSort: fields.name,
admin: { admin: {
useAsTitle: fields.slug, useAsTitle: fields.name,
defaultColumns: [fields.slug, fields.type, fields.translations], defaultColumns: [fields.name, fields.type, fields.translations],
group: CollectionGroup.Meta, group: CollectionGroups.Meta,
components: { components: {
BeforeListTable: [ BeforeListTable: [
() => () =>
QuickFilters({ QuickFilters({
route: "/admin/collections/keys", slug: Collections.Keys,
filters: [ filterGroups: [
{ label: "Wordings", filter: "where[type][equals]=Wordings" }, Object.entries(KeysTypes).map(([key, value]) => ({
{ label: "∅ English", filter: "where[translations.language][not_equals]=en" }, label: value,
{ label: "∅ French", filter: "where[translations.language][not_equals]=fr" }, filter: { where: { type: { equals: key } } },
})),
Object.entries(LanguageCodes).map(([key, value]) => ({
label: `${value}`,
filter: { where: { "translations.language": { not_equals: key } } },
})),
], ],
}), }),
], ],
}, },
hooks: { hooks: {
beforeDuplicate: beforeDuplicateAddCopyTo(fields.slug), beforeDuplicate: beforeDuplicateAddCopyTo(fields.name),
}, },
}, },
access: { access: {
create: mustBeAdmin, create: mustBeAdmin,
delete: mustBeAdmin, delete: mustBeAdmin,
}, },
hooks: {
beforeValidate: [
async ({ data: { name, type } }) => {
const result = await payload.find({
collection: Collections.Keys,
where: { name: { equals: name }, type: { equals: type } },
});
if (result.docs.length > 0) {
throw new Error(
`A Key of type "${KeysTypes[type]}" already exists with the name "${name}"`
);
}
},
],
},
endpoints: [importFromStrapi],
timestamps: false, timestamps: false,
versions: false, versions: false,
fields: [ fields: [
slugField({ name: fields.slug }), {
name: fields.name,
type: "text",
required: true,
},
{ {
name: fields.type, name: fields.type,
type: "select", type: "select",
@ -65,15 +91,20 @@ export const Keys: CollectionConfig = buildCollectionConfig(
name: fields.translations, name: fields.translations,
interfaceName: "CategoryTranslations", interfaceName: "CategoryTranslations",
admin: { admin: {
useAsTitle: fields.name, useAsTitle: fields.translationsName,
}, },
fields: [ fields: [
{ {
type: "row", type: "row",
fields: [ fields: [
{ name: fields.name, type: "text", required: true, admin: { width: "50%" } },
{ {
name: fields.short, name: fields.translationsName,
type: "text",
required: true,
admin: { width: "50%" },
},
{
name: fields.translationsShort,
type: "text", type: "text",
admin: { admin: {
condition: (data: Partial<Key>) => condition: (data: Partial<Key>) =>

View File

@ -0,0 +1,283 @@
import payload from "payload";
import { CollectionConfig } from "payload/types";
import { Collections } from "../../../constants";
import {
getAllStrapiEntries,
importStrapiEntries,
} from "../../../endpoints/createStrapiImportEndpoint";
import { Key } from "../../../types/collections";
import { isDefined } from "../../../utils/asserts";
import { formatToCamelCase } from "../../../utils/string";
import { PayloadCreateData } from "../../../utils/types";
const importStrapiWordings: typeof importStrapiEntries = async ({
payload: payloadParams,
strapi: strapiParams,
user,
}) => {
const rawEntries = await getAllStrapiEntries<any>(strapiParams.collection, strapiParams.params);
const { ui_language, createdAt, updatedAt, ...otherKeys } = rawEntries[0].attributes;
const entries: PayloadCreateData<Key>[] = Object.keys(otherKeys).map((key) => ({
name: formatToCamelCase(key),
type: "Wordings",
translations: rawEntries
.map((entry) => ({
language: entry.attributes.ui_language.data.attributes.code,
name: entry.attributes[key],
}))
.filter(({ name }) => isDefined(name) && name !== ""),
}));
const errors = [];
await Promise.all(
entries.map(async (entry) => {
try {
await payload.create({
collection: payloadParams.collection,
data: entry,
user,
});
} catch (e) {
console.warn(e);
errors.push(`${e.name} with ${entry.name}`);
}
})
);
return { count: entries.length, errors };
};
export const importFromStrapi: CollectionConfig["endpoints"][number] = {
method: "get",
path: "/strapi",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const { count: categoriesCount, errors: categoriesErrors } = await importStrapiEntries<Key>({
strapi: {
collection: "categories",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "Categories",
translations: titles.map(({ title, short, language }) => ({
name: title,
short,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
});
const { count: contentTypesCount, errors: contentTypesErrors } = await importStrapiEntries<Key>(
{
strapi: {
collection: "content-types",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "Contents",
translations: titles.map(({ title, language }) => ({
name: title,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
}
);
const { count: gamePlatformsCount, errors: gamePlatformsErrors } =
await importStrapiEntries<Key>({
strapi: {
collection: "game-platforms",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "GamePlatforms",
translations: titles.map(({ title, short, language }) => ({
name: title,
short,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
});
const { count: libraryCount, errors: libraryErrors } = await importStrapiEntries<Key>({
strapi: {
collection: "metadata-types",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "Library",
translations: titles.map(({ title, language }) => ({
name: title,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
});
const { count: libraryAudioCount, errors: libraryAudioErrors } = await importStrapiEntries<Key>(
{
strapi: {
collection: "audio-subtypes",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "LibraryAudio",
translations: titles.map(({ title, language }) => ({
name: title,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
}
);
const { count: libraryGroupCount, errors: libraryGroupErrors } = await importStrapiEntries<Key>(
{
strapi: {
collection: "group-subtypes",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "LibraryGroup",
translations: titles.map(({ title, language }) => ({
name: title,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
}
);
const { count: libraryTextualCount, errors: libraryTextualErrors } =
await importStrapiEntries<Key>({
strapi: {
collection: "textual-subtypes",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "LibraryTextual",
translations: titles.map(({ title, language }) => ({
name: title,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
});
const { count: libraryVideoCount, errors: libraryVideoErrors } = await importStrapiEntries<Key>(
{
strapi: {
collection: "video-subtypes",
params: { populate: { titles: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, titles }) => ({
name: slug,
type: "LibraryVideo",
translations: titles.map(({ title, language }) => ({
name: title,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
}
);
const { count: weaponsCount, errors: weaponsErrors } = await importStrapiEntries<Key>({
strapi: {
collection: "weapon-story-types",
params: { populate: { translations: { populate: "language" } } },
},
payload: {
collection: Collections.Keys,
convert: ({ slug, translations }) => ({
name: slug,
type: "Weapons",
translations: translations.map(({ name, language }) => ({
name,
language: language.data.attributes.code,
})),
}),
},
user: req.user,
});
const { count: wordingsCount, errors: wordingsErrors } = await importStrapiWordings({
strapi: { collection: "website-interfaces", params: { populate: "ui_language" } },
payload: { collection: Collections.Keys, convert: (strapiObject) => strapiObject },
user: req.user,
});
res.status(200).json({
message: `${
categoriesCount +
contentTypesCount +
gamePlatformsCount +
libraryCount +
libraryAudioCount +
libraryGroupCount +
libraryTextualCount +
libraryVideoCount +
weaponsCount +
wordingsCount
} entries have been added successfully.`,
errors: {
categoriesErrors,
contentTypesErrors,
gamePlatformsErrors,
libraryErrors,
libraryAudioErrors,
libraryGroupErrors,
libraryTextualErrors,
libraryVideoErrors,
weaponsErrors,
wordingsErrors,
},
});
},
};

View File

@ -1,6 +1,9 @@
import { text } from "payload/dist/fields/validations";
import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroup } from "../../constants"; import { publicAccess } from "../../accesses/publicAccess";
import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = { const fields = {
id: "id", id: "id",
@ -8,6 +11,7 @@ const fields = {
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Languages = buildCollectionConfig( export const Languages = buildCollectionConfig(
Collections.Languages,
{ {
singular: "Language", singular: "Language",
plural: "Languages", plural: "Languages",
@ -18,21 +22,23 @@ export const Languages = buildCollectionConfig(
useAsTitle: fields.name, useAsTitle: fields.name,
defaultColumns: [fields.name, fields.id], defaultColumns: [fields.name, fields.id],
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Meta, group: CollectionGroups.Meta,
pagination: { defaultLimit: 100 },
}, },
access: { create: mustBeAdmin, update: mustBeAdmin }, access: { create: mustBeAdmin, update: mustBeAdmin, read: publicAccess },
timestamps: false, timestamps: false,
endpoints: [importFromStrapi],
fields: [ fields: [
{ {
name: fields.id, name: fields.id,
type: "text", type: "text",
unique: true, unique: true,
required: true, required: true,
validate: (value) => { validate: (value, options) => {
if (/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { if (!/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) {
return true; return "The code must be a valid BCP 47 language tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)";
} }
return "The code must be a valid IETF language tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)"; return text(value, options);
}, },
}, },
{ {

View File

@ -0,0 +1,14 @@
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { Language } from "../../../types/collections";
export const importFromStrapi = createStrapiImportEndpoint<Language>({
strapi: {
collection: "languages",
params: {},
},
payload: {
collection: Collections.Languages,
convert: ({ code, name }) => ({ id: code, name }),
},
});

View File

@ -1,24 +1,21 @@
import { import {
CollectionGroup, CollectionGroups,
Collections,
KeysTypes, KeysTypes,
LibraryItemsTextualBindingTypes, LibraryItemsTextualBindingTypes,
LibraryItemsTextualPageOrders, LibraryItemsTextualPageOrders,
LibraryItemsTypes, LibraryItemsTypes,
} from "../../constants"; } from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { LibraryItemThumbnails } from "../LibraryItemThumbnails/LibraryItemThumbnails";
import { LibraryItem } from "../../types/collections";
import { Keys } from "../Keys/Keys";
import { Languages } from "../Languages/Languages";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { Currencies } from "../Currencies/Currencies";
import { optionalGroupField } from "../../fields/optionalGroupField/optionalGroupField"; import { optionalGroupField } from "../../fields/optionalGroupField/optionalGroupField";
import { slugField } from "../../fields/slugField/slugField";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { LibraryItem } from "../../types/collections";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { RowLabel } from "./components/RowLabel"; import { RowLabel } from "./components/RowLabel";
import { getSlug } from "./endpoints/getSlug"; import { getBySlug } from "./endpoints/getBySlug";
const fields = { const fields = {
status: "status", status: "status",
@ -57,21 +54,29 @@ const fields = {
scansDustjacketFront: "front", scansDustjacketFront: "front",
scansDustjacketSpine: "spine", scansDustjacketSpine: "spine",
scansDustjacketBack: "back", scansDustjacketBack: "back",
scansObibelt: "obibelt", scansObi: "obi",
scansObibeltFront: "front", scansObiFront: "front",
scansObibeltSpine: "spine", scansObiSpine: "spine",
scansObibeltBack: "back", scansObiBack: "back",
scansPages: "pages", scansPages: "pages",
scansPagesPage: "page", scansPagesPage: "page",
scansPagesImage: "image", scansPagesImage: "image",
contents: "contents",
contentsContent: "content",
contentsPageStart: "pageStart",
contentsPageEnd: "pageEnd",
contentsTimeStart: "timeStart",
contentsTimeEnd: "timeEnd",
contentsNote: "note",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const LibraryItems = buildVersionedCollectionConfig( export const LibraryItems = buildVersionedCollectionConfig(
Collections.LibraryItems,
{ {
singular: "Library Item", singular: "Library Item",
plural: "Library Items", plural: "Library Items",
}, },
({ slug }) => ({ () => ({
defaultSort: fields.slug, defaultSort: fields.slug,
admin: { admin: {
useAsTitle: fields.slug, useAsTitle: fields.slug,
@ -79,7 +84,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
"A comprehensive list of all Yokoverses side materials (books, novellas, artbooks, \ "A comprehensive list of all Yokoverses side materials (books, novellas, artbooks, \
stage plays, manga, drama CDs, and comics).", stage plays, manga, drama CDs, and comics).",
defaultColumns: [fields.slug, fields.thumbnail, fields.status], defaultColumns: [fields.slug, fields.thumbnail, fields.status],
group: CollectionGroup.Collections, group: CollectionGroups.Collections,
hooks: { hooks: {
beforeDuplicate: beforeDuplicatePiping([ beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish, beforeDuplicateUnpublish,
@ -88,7 +93,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
}, },
preview: (doc) => `https://accords-library.com/library/${doc.slug}`, preview: (doc) => `https://accords-library.com/library/${doc.slug}`,
}, },
endpoints: [getSlug(slug)], endpoints: [getBySlug],
fields: [ fields: [
{ {
type: "row", type: "row",
@ -96,7 +101,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
slugField({ name: fields.slug, admin: { width: "50%" } }), slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({ imageField({
name: fields.thumbnail, name: fields.thumbnail,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "50%" }, admin: { width: "50%" },
}), }),
], ],
@ -167,17 +172,17 @@ export const LibraryItems = buildVersionedCollectionConfig(
fields: [ fields: [
imageField({ imageField({
name: fields.scansCoverFront, name: fields.scansCoverFront,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
imageField({ imageField({
name: fields.scansCoverSpine, name: fields.scansCoverSpine,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
imageField({ imageField({
name: fields.scansCoverBack, name: fields.scansCoverBack,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
], ],
@ -188,23 +193,28 @@ export const LibraryItems = buildVersionedCollectionConfig(
name: fields.scansDustjacket, name: fields.scansDustjacket,
label: "Dust Jacket", label: "Dust Jacket",
labels: { singular: "Dust Jacket", plural: "Dust Jackets" }, labels: { singular: "Dust Jacket", plural: "Dust Jackets" },
admin: {
description:
"The dust jacket of a book is the detachable outer cover with folded \
flaps that hold it to the front and back book covers",
},
fields: [ fields: [
{ {
type: "row", type: "row",
fields: [ fields: [
imageField({ imageField({
name: fields.scansDustjacketFront, name: fields.scansDustjacketFront,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
imageField({ imageField({
name: fields.scansDustjacketSpine, name: fields.scansDustjacketSpine,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
imageField({ imageField({
name: fields.scansDustjacketBack, name: fields.scansDustjacketBack,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
], ],
@ -212,26 +222,31 @@ export const LibraryItems = buildVersionedCollectionConfig(
], ],
}), }),
optionalGroupField({ optionalGroupField({
name: fields.scansObibelt, name: fields.scansObi,
label: "Obi Belt", label: "Obi",
labels: { singular: "Obi Belt", plural: "Obi Belts" }, labels: { singular: "Obi Belt", plural: "Obi Belts" },
admin: {
description:
"An obi is a strip of paper looped around a book or other product. \
it typically add marketing claims, or other relevant information about the product.",
},
fields: [ fields: [
{ {
type: "row", type: "row",
fields: [ fields: [
imageField({ imageField({
name: fields.scansObibeltFront, name: fields.scansObiFront,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
imageField({ imageField({
name: fields.scansObibeltSpine, name: fields.scansObiSpine,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
imageField({ imageField({
name: fields.scansObibeltBack, name: fields.scansObiBack,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
admin: { width: "33%" }, admin: { width: "33%" },
}), }),
], ],
@ -243,6 +258,9 @@ export const LibraryItems = buildVersionedCollectionConfig(
type: "array", type: "array",
admin: { admin: {
initCollapsed: true, initCollapsed: true,
description:
"Make sure the page number corresponds to the page number written on \
the scan. You can use negative page numbers if necessary.",
components: { components: {
RowLabel: ({ data }) => RowLabel(data), RowLabel: ({ data }) => RowLabel(data),
}, },
@ -259,7 +277,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
}, },
imageField({ imageField({
name: fields.scansPagesImage, name: fields.scansPagesImage,
relationTo: LibraryItemThumbnails.slug, relationTo: Collections.LibraryItemsThumbnails,
required: true, required: true,
admin: { width: "66%" }, admin: { width: "66%" },
}), }),
@ -314,7 +332,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
{ {
name: fields.priceCurrency, name: fields.priceCurrency,
type: "relationship", type: "relationship",
relationTo: Currencies.slug, relationTo: Collections.Currencies,
required: true, required: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
}, },
@ -344,7 +362,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
name: fields.textualSubtype, name: fields.textualSubtype,
label: "Subtype", label: "Subtype",
type: "relationship", type: "relationship",
relationTo: [Keys.slug], relationTo: [Collections.Keys],
filterOptions: { type: { equals: KeysTypes.LibraryTextual } }, filterOptions: { type: { equals: KeysTypes.LibraryTextual } },
hasMany: true, hasMany: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
@ -352,7 +370,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
{ {
name: fields.textualLanguages, name: fields.textualLanguages,
type: "relationship", type: "relationship",
relationTo: [Languages.slug], relationTo: [Collections.Languages],
hasMany: true, hasMany: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
}, },
@ -406,7 +424,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
name: fields.audioSubtype, name: fields.audioSubtype,
label: "Subtype", label: "Subtype",
type: "relationship", type: "relationship",
relationTo: [Keys.slug], relationTo: [Collections.Keys],
filterOptions: { type: { equals: KeysTypes.LibraryAudio } }, filterOptions: { type: { equals: KeysTypes.LibraryAudio } },
hasMany: true, hasMany: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
@ -415,6 +433,57 @@ export const LibraryItems = buildVersionedCollectionConfig(
}, },
], ],
}, },
{
name: fields.contents,
type: "array",
fields: [
{
name: fields.contentsContent,
type: "relationship",
relationTo: Collections.Contents,
required: true,
},
{
type: "row",
admin: {
condition: ({ itemType }) => {
return itemType === LibraryItemsTypes.Textual;
},
},
fields: [
{
name: fields.contentsPageStart,
type: "number",
},
{ name: fields.contentsPageEnd, type: "number" },
],
},
{
type: "row",
admin: {
condition: ({ itemType }) => {
return itemType === LibraryItemsTypes.Audio || itemType === LibraryItemsTypes.Video;
},
},
fields: [
{
name: fields.contentsTimeStart,
type: "number",
},
{ name: fields.contentsTimeEnd, type: "number" },
],
},
{
name: fields.contentsNote,
type: "textarea",
admin: {
condition: ({ itemType }) => {
return itemType === LibraryItemsTypes.Game || itemType === LibraryItemsTypes.Other;
},
},
},
],
},
{ {
name: fields.releaseDate, name: fields.releaseDate,
type: "date", type: "date",

View File

@ -1,20 +1,22 @@
import { LibraryItems } from "../LibraryItems";
import { LibraryItem } from "../../../types/collections";
import cleanDeep from "clean-deep"; import cleanDeep from "clean-deep";
import { createBySlugEndpoint } from "../../../endpoints/createBySlugEndpoint"; import { Collections } from "../../../constants";
import { createGetByEndpoint } from "../../../endpoints/createByEndpoint";
import { LibraryItem } from "../../../types/collections";
type ProcessedLibraryItem = Omit<LibraryItem, "size" | "price" | "scans" | "id"> & { type ProcessedLibraryItem = Omit<LibraryItem, "size" | "price" | "scans" | "id"> & {
size?: Omit<LibraryItem["size"][number], "id">; size?: Omit<LibraryItem["size"][number], "id">;
price?: Omit<LibraryItem["price"][number], "id" | "currency"> & { currency: string }; price?: Omit<LibraryItem["price"][number], "id" | "currency"> & { currency: string };
scans?: Omit<LibraryItem["scans"][number], "id" | "obibelt" | "cover" | "dustjacket"> & { scans?: Omit<LibraryItem["scans"][number], "id" | "obi" | "cover" | "dustjacket"> & {
obibelt: Omit<LibraryItem["scans"][number]["obibelt"][number], "id">; obi: Omit<LibraryItem["scans"][number]["obi"][number], "id">;
cover: Omit<LibraryItem["scans"][number]["obibelt"][number], "id">; cover: Omit<LibraryItem["scans"][number]["obi"][number], "id">;
dustjacket: Omit<LibraryItem["scans"][number]["obibelt"][number], "id">; dustjacket: Omit<LibraryItem["scans"][number]["obi"][number], "id">;
}; };
}; };
export const getSlug = (collectionSlug: string) => export const getBySlug = createGetByEndpoint<LibraryItem, Partial<ProcessedLibraryItem>>(
createBySlugEndpoint<LibraryItem>(collectionSlug, ({ id, size, price, scans, ...otherProps }) => { Collections.LibraryItems,
"slug",
async ({ id, size, price, scans, ...otherProps }) => {
const processedLibraryItem: ProcessedLibraryItem = { const processedLibraryItem: ProcessedLibraryItem = {
size: processOptionalGroup(size), size: processOptionalGroup(size),
price: processPrice(price), price: processPrice(price),
@ -30,15 +32,16 @@ export const getSlug = (collectionSlug: string) =>
undefinedValues: true, undefinedValues: true,
NaNValues: false, NaNValues: false,
}); });
}); }
);
const processScans = (scans: LibraryItem["scans"]): ProcessedLibraryItem["scans"] => { const processScans = (scans: LibraryItem["scans"]): ProcessedLibraryItem["scans"] => {
if (!scans || scans.length === 0) return undefined; if (!scans || scans.length === 0) return undefined;
const { cover, dustjacket, id, obibelt, ...otherProps } = scans[0]; const { cover, dustjacket, id, obi, ...otherProps } = scans[0];
return { return {
cover: processOptionalGroup(cover), cover: processOptionalGroup(cover),
dustjacket: processOptionalGroup(dustjacket), dustjacket: processOptionalGroup(dustjacket),
obibelt: processOptionalGroup(obibelt), obi: processOptionalGroup(obi),
...otherProps, ...otherProps,
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CollectionGroup } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -7,7 +7,8 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const LibraryItemThumbnails = buildCollectionConfig( export const LibraryItemsThumbnails = buildCollectionConfig(
Collections.LibraryItemsThumbnails,
{ {
singular: "Library Item Thumbnail", singular: "Library Item Thumbnail",
plural: "Library Item Thumbnails", plural: "Library Item Thumbnails",
@ -17,7 +18,7 @@ export const LibraryItemThumbnails = buildCollectionConfig(
admin: { admin: {
useAsTitle: fields.filename, useAsTitle: fields.filename,
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Media, group: CollectionGroups.Media,
}, },
upload: { upload: {
staticDir: uploadDir, staticDir: uploadDir,

View File

@ -1,16 +1,14 @@
import { slugField } from "../../fields/slugField/slugField"; import { QuickFilters, publishStatusFilters } from "../../components/QuickFilters";
import { CollectionGroups, Collections, KeysTypes } from "../../constants";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { CollectionGroup, KeysTypes } from "../../constants"; import { slugField } from "../../fields/slugField/slugField";
import { Recorders } from "../Recorders/Recorders";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { isDefined, isUndefined } from "../../utils/asserts"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate";
import { Keys } from "../Keys/Keys";
import { PostThumbnails } from "../PostThumbnails/PostThumbnails";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { isDefined, isUndefined } from "../../utils/asserts";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate";
const fields = { const fields = {
slug: "slug", slug: "slug",
@ -29,6 +27,7 @@ const fields = {
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Posts = buildVersionedCollectionConfig( export const Posts = buildVersionedCollectionConfig(
Collections.Posts,
{ {
singular: "Post", singular: "Post",
plural: "Posts", plural: "Posts",
@ -41,7 +40,16 @@ export const Posts = buildVersionedCollectionConfig(
"News articles written by our Recorders! Here you will find announcements about \ "News articles written by our Recorders! Here you will find announcements about \
new merch/items releases, guides, theories, unboxings, showcases...", new merch/items releases, guides, theories, unboxings, showcases...",
defaultColumns: [fields.slug, fields.thumbnail, fields.categories], defaultColumns: [fields.slug, fields.thumbnail, fields.categories],
group: CollectionGroup.Collections, group: CollectionGroups.Collections,
components: {
BeforeListTable: [
() =>
QuickFilters({
slug: Collections.Posts,
filterGroups: [publishStatusFilters],
}),
],
},
hooks: { hooks: {
beforeDuplicate: beforeDuplicatePiping([ beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish, beforeDuplicateUnpublish,
@ -60,7 +68,7 @@ export const Posts = buildVersionedCollectionConfig(
slugField({ name: fields.slug, admin: { width: "50%" } }), slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({ imageField({
name: fields.thumbnail, name: fields.thumbnail,
relationTo: PostThumbnails.slug, relationTo: Collections.PostsThumbnails,
admin: { width: "50%" }, admin: { width: "50%" },
}), }),
], ],
@ -71,7 +79,7 @@ export const Posts = buildVersionedCollectionConfig(
{ {
name: fields.authors, name: fields.authors,
type: "relationship", type: "relationship",
relationTo: [Recorders.slug], relationTo: [Collections.Recorders],
required: true, required: true,
minRows: 1, minRows: 1,
hasMany: true, hasMany: true,
@ -80,7 +88,7 @@ export const Posts = buildVersionedCollectionConfig(
{ {
name: fields.categories, name: fields.categories,
type: "relationship", type: "relationship",
relationTo: [Keys.slug], relationTo: [Collections.Keys],
filterOptions: { type: { equals: KeysTypes.Categories } }, filterOptions: { type: { equals: KeysTypes.Categories } },
hasMany: true, hasMany: true,
admin: { allowCreate: false, width: "35%" }, admin: { allowCreate: false, width: "35%" },
@ -101,7 +109,7 @@ export const Posts = buildVersionedCollectionConfig(
{ {
name: fields.translators, name: fields.translators,
type: "relationship", type: "relationship",
relationTo: Recorders.slug, relationTo: Collections.Recorders,
hasMany: true, hasMany: true,
admin: { admin: {
condition: (_, siblingData) => { condition: (_, siblingData) => {
@ -134,7 +142,7 @@ export const Posts = buildVersionedCollectionConfig(
{ {
name: fields.proofreaders, name: fields.proofreaders,
type: "relationship", type: "relationship",
relationTo: Recorders.slug, relationTo: Collections.Recorders,
hasMany: true, hasMany: true,
admin: { width: "50%" }, admin: { width: "50%" },
}, },

View File

@ -1,4 +1,4 @@
import { CollectionGroup } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -7,7 +7,8 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const PostThumbnails = buildCollectionConfig( export const PostsThumbnails = buildCollectionConfig(
Collections.PostsThumbnails,
{ {
singular: "Post Thumbnail", singular: "Post Thumbnail",
plural: "Post Thumbnails", plural: "Post Thumbnails",
@ -17,7 +18,7 @@ export const PostThumbnails = buildCollectionConfig(
admin: { admin: {
useAsTitle: fields.filename, useAsTitle: fields.filename,
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Media, group: CollectionGroups.Media,
}, },
upload: { upload: {
staticDir: uploadDir, staticDir: uploadDir,

View File

@ -1,13 +1,12 @@
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { Languages } from "../Languages/Languages";
import { CollectionGroup, RecordersRoles } from "../../constants";
import { RecorderThumbnails } from "../RecorderThumbnails/RecorderThumbnails";
import { imageField } from "../../fields/imageField/imageField";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { mustBeAdminOrSelf } from "../../accesses/collections/mustBeAdminOrSelf"; import { mustBeAdminOrSelf } from "../../accesses/collections/mustBeAdminOrSelf";
import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole"; import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { QuickFilters } from "../../components/QuickFilters"; import { QuickFilters } from "../../components/QuickFilters";
import { CollectionGroups, Collections, RecordersRoles } from "../../constants";
import { imageField } from "../../fields/imageField/imageField";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole";
const fields = { const fields = {
username: "username", username: "username",
@ -20,6 +19,7 @@ const fields = {
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Recorders = buildCollectionConfig( export const Recorders = buildCollectionConfig(
Collections.Recorders,
{ {
singular: "Recorder", singular: "Recorder",
plural: "Recorders", plural: "Recorders",
@ -40,17 +40,25 @@ export const Recorders = buildCollectionConfig(
fields.role, fields.role,
], ],
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Meta, group: CollectionGroups.Meta,
components: { components: {
BeforeListTable: [ BeforeListTable: [
() => () =>
QuickFilters({ QuickFilters({
route: "/admin/collections/recorders", slug: Collections.Recorders,
filters: [ filterGroups: [
{ label: "Admins", filter: "where[role][equals]=Admin" }, [
{ label: "Recorders", filter: "where[role][equals]=Recorder" }, ...Object.entries(RecordersRoles).map(([key, value]) => ({
{ label: "∅ Role", filter: "where[role][not_in]=Admin,Recorder" }, label: value,
{ label: "Anonymized", filter: "where[anonymize][equals]=true" }, filter: { where: { role: { equals: key } } },
})),
{
label: "∅ Role",
filter: { where: { role: { not_in: Object.keys(RecordersRoles).join(",") } } },
},
,
],
[{ label: "Anonymized", filter: { where: { anonymize: { equals: true } } } }],
], ],
}), }),
], ],
@ -66,6 +74,7 @@ export const Recorders = buildCollectionConfig(
hooks: { hooks: {
beforeLogin: [beforeLoginMustHaveAtLeastOneRole], beforeLogin: [beforeLoginMustHaveAtLeastOneRole],
}, },
endpoints: [importFromStrapi],
timestamps: false, timestamps: false,
fields: [ fields: [
{ {
@ -80,7 +89,7 @@ export const Recorders = buildCollectionConfig(
}, },
imageField({ imageField({
name: fields.avatar, name: fields.avatar,
relationTo: RecorderThumbnails.slug, relationTo: Collections.RecordersThumbnails,
admin: { width: "66%" }, admin: { width: "66%" },
}), }),
], ],
@ -88,7 +97,7 @@ export const Recorders = buildCollectionConfig(
{ {
name: fields.languages, name: fields.languages,
type: "relationship", type: "relationship",
relationTo: Languages.slug, relationTo: Collections.Languages,
hasMany: true, hasMany: true,
admin: { admin: {
allowCreate: false, allowCreate: false,

View File

@ -0,0 +1,39 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { Recorder } from "../../../types/collections";
import { uploadStrapiImage } from "../../../utils/localApi";
import { PayloadCreateData } from "../../../utils/types";
export const importFromStrapi = createStrapiImportEndpoint<Recorder>({
strapi: {
collection: "recorders",
params: {
populate: "bio.language,languages,avatar",
},
},
payload: {
collection: Collections.Recorders,
import: async ({ username, anonymize, anonymous_code, languages, avatar, bio: bios }, user) => {
const avatarId = await uploadStrapiImage({
collection: Collections.RecordersThumbnails,
image: avatar,
});
const data: PayloadCreateData<Recorder> = {
email: `${anonymous_code}@accords-library.com`,
password: process.env.RECORDER_DEFAULT_PASSWORD,
username,
anonymize,
languages: languages.data?.map((language) => language.attributes.code),
avatar: avatarId,
biographies: bios?.map(({ language, bio }) => ({
language: language.data.attributes.code,
biography: bio,
})),
};
await payload.create({ collection: Collections.Recorders, data, user });
},
},
});

View File

@ -0,0 +1,59 @@
import { CollectionGroups, Collections } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = {
filename: "filename",
mimeType: "mimeType",
filesize: "filesize",
recorder: "recorder",
} as const satisfies Record<string, string>;
export const RecordersThumbnails = buildCollectionConfig(
Collections.RecordersThumbnails,
{
singular: "Recorders Thumbnail",
plural: "Recorders Thumbnails",
},
({ uploadDir }) => ({
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
disableDuplicate: true,
group: CollectionGroups.Media,
},
upload: {
staticDir: uploadDir,
adminThumbnail: "small",
mimeTypes: ["image/*"],
imageSizes: [
{
name: "og",
height: 256,
width: 256,
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
},
},
{
name: "small",
height: 128,
width: 128,
formatOptions: {
format: "webp",
options: { effort: 6, quality: 80, alphaQuality: 80 },
},
},
],
},
fields: [
backPropagationField({
name: fields.recorder,
hasMany: false,
relationTo: Collections.Recorders,
where: (id) => ({ avatar: { equals: id } }),
}),
],
})
);

View File

@ -0,0 +1,103 @@
import { CollectionConfig } from "payload/types";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroups, Collections, VideoSources } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
uid: "uid",
gone: "gone",
source: "source",
liveChat: "liveChat",
title: "title",
description: "description",
publishedDate: "publishedDate",
views: "views",
likes: "likes",
channel: "channel",
} as const satisfies Record<string, string>;
export const Videos: CollectionConfig = buildCollectionConfig(
Collections.Videos,
{
singular: "Video",
plural: "Videos",
},
() => ({
defaultSort: fields.uid,
admin: {
useAsTitle: fields.title,
defaultColumns: [
fields.uid,
fields.title,
fields.source,
fields.gone,
fields.liveChat,
fields.publishedDate,
fields.views,
fields.likes,
fields.channel,
],
group: CollectionGroups.Media,
disableDuplicate: true,
},
access: {
create: mustBeAdmin,
delete: mustBeAdmin,
},
endpoints: [importFromStrapi],
timestamps: false,
fields: [
{
type: "row",
fields: [
{ name: fields.uid, type: "text", required: true, unique: true, admin: { width: "33%" } },
{
name: fields.gone,
type: "checkbox",
defaultValue: false,
required: true,
admin: {
description:
"Is the video no longer available (deleted, privatized, unlisted, blocked...)",
width: "33%",
},
},
{
name: fields.source,
type: "select",
required: true,
options: Object.entries(VideoSources).map(([value, label]) => ({ label, value })),
admin: { width: "33%" },
},
],
},
{ name: fields.title, type: "text", required: true },
{ name: fields.description, type: "textarea" },
{
type: "row",
fields: [
{ name: fields.likes, type: "number", admin: { width: "50%" } },
{ name: fields.views, type: "number", admin: { width: "50%" } },
],
},
{
name: fields.publishedDate,
type: "date",
admin: {
date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" },
position: "sidebar",
},
required: true,
},
{
name: fields.channel,
type: "relationship",
relationTo: Collections.VideosChannels,
required: true,
admin: { position: "sidebar" },
},
],
})
);

View File

@ -0,0 +1,70 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { Video, VideosChannel } from "../../../types/collections";
import { PayloadCreateData } from "../../../utils/types";
export const importFromStrapi = createStrapiImportEndpoint<Video>({
strapi: {
collection: "videos",
params: { populate: "published_date,channel" },
},
payload: {
collection: Collections.Videos,
import: async (
{
uid,
title,
description,
views,
likes,
gone,
source,
published_date: { year, month, day },
channel,
},
user
) => {
try {
const videoChannel: PayloadCreateData<VideosChannel> = {
uid: channel.data.attributes.uid,
title: channel.data.attributes.title,
subscribers: channel.data.attributes.subscribers,
};
await payload.create({
collection: Collections.VideosChannels,
data: videoChannel,
user,
});
} catch (e) {}
const result = await payload.find({
collection: Collections.VideosChannels,
where: { uid: { equals: channel.data.attributes.uid } },
});
if (result.docs.length === 0) {
throw new Error("A video channel is required to create a video");
}
const videoChannel = result.docs[0] as VideosChannel;
const video: PayloadCreateData<Video> = {
uid,
title,
description,
views,
likes,
gone,
source,
publishedDate: `${year}-${month}-${day}`,
channel: videoChannel.id,
};
await payload.create({
collection: Collections.Videos,
data: video,
user,
});
},
},
});

View File

@ -0,0 +1,45 @@
import { CollectionConfig } from "payload/types";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
uid: "uid",
title: "title",
subscribers: "subscribers",
videos: "videos",
} as const satisfies Record<string, string>;
export const VideosChannels: CollectionConfig = buildCollectionConfig(
Collections.VideosChannels,
{
singular: "Videos Channel",
plural: "Videos Channels",
},
() => ({
defaultSort: fields.title,
admin: {
useAsTitle: fields.title,
defaultColumns: [fields.uid, fields.title, fields.subscribers, fields.videos],
group: CollectionGroups.Media,
disableDuplicate: true,
},
access: {
create: mustBeAdmin,
delete: mustBeAdmin,
},
endpoints: [importFromStrapi],
timestamps: false,
fields: [
{ name: fields.uid, type: "text", required: true, unique: true },
{
type: "row",
fields: [
{ name: fields.title, type: "text", required: true },
{ name: fields.subscribers, type: "number" },
],
},
],
})
);

View File

@ -0,0 +1,18 @@
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { VideosChannel } from "../../../types/collections";
export const importFromStrapi = createStrapiImportEndpoint<VideosChannel>({
strapi: {
collection: "video-channels",
params: {},
},
payload: {
collection: Collections.VideosChannels,
convert: ({ uid, title, subscribers }) => ({
uid,
title,
subscribers,
}),
},
});

View File

@ -0,0 +1,163 @@
import { CollectionGroups, Collections, KeysTypes } from "../../constants";
import { imageField } from "../../fields/imageField/imageField";
import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { AppearanceRowLabel } from "./components/AppearanceRowLabel";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
slug: "slug",
thumbnail: "thumbnail",
type: "type",
group: "group",
appearances: "appearances",
appearancesCategories: "categories",
appearancesTranslations: "translations",
appearancesTranslationsName: "name",
appearancesTranslationsDescription: "description",
appearancesTranslationsLevel1: "level1",
appearancesTranslationsLevel2: "level2",
appearancesTranslationsLevel3: "level3",
appearancesTranslationsLevel4: "level4",
status: "_status",
};
export const Weapons = buildVersionedCollectionConfig(
Collections.Weapons,
{ singular: "Weapon", plural: "Weapons" },
() => ({
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [
fields.slug,
fields.thumbnail,
fields.group,
fields.type,
fields.appearances,
fields.status,
],
group: CollectionGroups.Collections,
},
endpoints: [importFromStrapi],
fields: [
{
type: "row",
fields: [
slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({
name: fields.thumbnail,
relationTo: Collections.WeaponsThumbnails,
admin: { width: "50%" },
}),
],
},
{
type: "row",
fields: [
{
name: fields.type,
type: "relationship",
relationTo: Collections.Keys,
required: true,
filterOptions: { type: { equals: KeysTypes.Weapons } },
admin: { allowCreate: false, width: "50%" },
},
{
name: fields.group,
type: "relationship",
relationTo: Collections.WeaponsGroups,
admin: { width: "50%" },
},
],
},
{
name: fields.appearances,
type: "array",
required: true,
minRows: 1,
admin: {
initCollapsed: true,
components: {
RowLabel: ({ data }) =>
AppearanceRowLabel({ keyIds: data[fields.appearancesCategories] ?? [] }),
},
},
fields: [
{
name: fields.appearancesCategories,
type: "relationship",
required: true,
hasMany: true,
relationTo: Collections.Keys,
filterOptions: { type: { equals: KeysTypes.Categories } },
admin: { allowCreate: false },
},
localizedFields({
name: fields.appearancesTranslations,
required: true,
minRows: 1,
admin: {
useAsTitle: fields.appearancesTranslationsName,
hasSourceLanguage: true,
hasCredits: true,
},
fields: [
{
type: "row",
fields: [
{
name: fields.appearancesTranslationsName,
type: "text",
required: true,
admin: { width: "50%" },
},
{
name: fields.appearancesTranslationsDescription,
type: "textarea",
admin: { width: "50%" },
},
],
},
{
type: "row",
fields: [
{
name: fields.appearancesTranslationsLevel1,
label: "Level 1",
type: "textarea",
admin: { width: "50%" },
},
{
name: fields.appearancesTranslationsLevel2,
label: "Level 2",
type: "textarea",
admin: { width: "50%" },
},
],
},
{
type: "row",
fields: [
{
name: fields.appearancesTranslationsLevel3,
label: "Level 3",
type: "textarea",
admin: { width: "50%" },
},
{
name: fields.appearancesTranslationsLevel4,
label: "Level 4",
type: "textarea",
admin: { width: "50%" },
},
],
},
],
}),
],
},
],
})
);

View File

@ -0,0 +1,36 @@
import React, { useEffect, useState } from "react";
import { styled } from "styled-components";
import { Collections } from "../../../constants";
interface Props {
keyIds: string[];
}
const Container = styled.div`
display: flex;
place-items: center;
gap: 5px;
`;
export const AppearanceRowLabel = ({ keyIds }: Props): JSX.Element => {
const [keySlugs, setKeySlugs] = useState<string[]>([]);
useEffect(() => {
const fetchUrl = async () => {
const results = await Promise.all(
keyIds.map(async (keyId) => await (await fetch(`/api/${Collections.Keys}/${keyId}`)).json())
);
setKeySlugs(results.map((result) => result.name));
};
fetchUrl();
}, [keyIds]);
return (
<Container>
{keySlugs.map((keySlug) => (
<div id={keySlug} className="pill pill--style-white">
{keySlug}
</div>
))}
</Container>
);
};

View File

@ -0,0 +1,86 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
import { Weapon, WeaponsGroup } from "../../../types/collections";
import { isDefined } from "../../../utils/asserts";
import { findCategory, findWeaponType, uploadStrapiImage } from "../../../utils/localApi";
import { PayloadCreateData } from "../../../utils/types";
export const importFromStrapi = createStrapiImportEndpoint<Weapon>({
strapi: {
collection: "weapon-stories",
params: {
populate: [
"name.language",
"type",
"weapon_group",
"stories.categories",
"stories.translations.language",
"thumbnail",
].join(),
},
},
payload: {
collection: Collections.Weapons,
import: async ({ slug, type, stories, name: names, weapon_group, thumbnail }, user) => {
let groupId: string;
if (isDefined(weapon_group.data)) {
try {
const groupData: PayloadCreateData<WeaponsGroup> = {
slug: weapon_group.data.attributes.slug,
};
await payload.create({
collection: Collections.WeaponsGroups,
data: groupData,
user,
});
} catch (e) {}
const result = await payload.find({
collection: Collections.WeaponsGroups,
where: { slug: { equals: weapon_group.data.attributes.slug } },
});
if (result.docs.length > 0) {
groupId = result.docs[0].id;
}
}
const thumbnailId = await uploadStrapiImage({
collection: Collections.WeaponsThumbnails,
image: thumbnail,
});
const data: PayloadCreateData<Weapon> = {
slug,
type: await findWeaponType(type.data.attributes.slug),
group: groupId,
thumbnail: thumbnailId,
appearances: await Promise.all(
stories.map(async ({ categories, translations }) => ({
categories: await Promise.all(
categories.data.map(({ attributes }) => findCategory(attributes.slug))
),
translations: translations.map(
({ language, description, level_1, level_2, level_3, level_4 }) => ({
language: language.data?.attributes.code,
sourceLanguage: language.data?.attributes.code,
name: names.find(
(name) => name.language.data?.attributes.code === language.data?.attributes.code
)?.name,
description,
level1: level_1,
level2: level_2,
level3: level_3,
level4: level_4,
transcribers: [user.id],
})
),
}))
),
};
await payload.create({ collection: Collections.Weapons, data, user });
},
},
});

View File

@ -0,0 +1,41 @@
import { CollectionGroups, Collections } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = {
slug: "slug",
translations: "translations",
translationsName: "name",
subgroupOf: "subgroupOf",
weapons: "weapons",
};
export const WeaponsGroups = buildCollectionConfig(
Collections.WeaponsGroups,
{ singular: "Weapons Group", plural: "Weapon Groups" },
() => ({
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.translations, fields.weapons, fields.subgroupOf],
group: CollectionGroups.Collections,
},
timestamps: false,
fields: [
slugField({ name: fields.slug }),
localizedFields({
name: fields.translations,
admin: { useAsTitle: fields.translationsName },
fields: [{ name: fields.translationsName, type: "text", required: true }],
}),
backPropagationField({
name: fields.weapons,
relationTo: Collections.Weapons,
hasMany: true,
where: (id) => ({ group: { equals: id } }),
}),
],
})
);

View File

@ -1,4 +1,4 @@
import { CollectionGroup } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -7,17 +7,18 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const RecorderThumbnails = buildCollectionConfig( export const WeaponsThumbnails = buildCollectionConfig(
Collections.WeaponsThumbnails,
{ {
singular: "Recorder Thumbnail", singular: "Weapons Thumbnail",
plural: "Recorder Thumbnails", plural: "Weapons Thumbnails",
}, },
({ uploadDir }) => ({ ({ uploadDir }) => ({
defaultSort: fields.filename, defaultSort: fields.filename,
admin: { admin: {
useAsTitle: fields.filename, useAsTitle: fields.filename,
disableDuplicate: true, disableDuplicate: true,
group: CollectionGroup.Media, group: CollectionGroups.Media,
}, },
upload: { upload: {
staticDir: uploadDir, staticDir: uploadDir,

View File

@ -1,22 +1,34 @@
import React from "react"; import React from "react";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import QueryString from "qs";
import { Options } from "payload/dist/collections/operations/local/find";
import { LanguageCodes } from "../constants";
type Props = { type Props = {
route: string; slug: string;
filters: { label: string; filter: string }[]; filterGroups: { label: string; filter: Omit<Options<any>, "collection"> }[][];
}; };
export const QuickFilters = ({ route, filters }: Props) => { export const QuickFilters = ({ slug, filterGroups }: Props) => {
const route = `/admin/collections/${slug}`;
return ( return (
<Container> <Container>
<div>Quick Filters:</div> <div>Quick Filters:</div>
<FilterContainer> <GroupContainer>
{filterGroups.map((filtersGroup, groupIndex) => (
<FilterContainer key={groupIndex}>
<FilterCell label="None" to={route} /> <FilterCell label="None" to={route} />
{filters.map(({ label, filter }, index) => ( {filtersGroup.map(({ label, filter }, index) => (
<FilterCell key={index} label={label} to={`${route}?${filter}`} /> <FilterCell
key={index}
label={label}
to={`${route}?${QueryString.stringify(filter)}`}
/>
))} ))}
</FilterContainer> </FilterContainer>
))}
</GroupContainer>
</Container> </Container>
); );
}; };
@ -32,6 +44,11 @@ const FilterCell = ({ label, to }: FilterProps) => (
</Link> </Link>
); );
const GroupContainer = styled.div`
display: grid;
gap: 4px;
`;
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
place-items: center; place-items: center;
@ -42,6 +59,18 @@ const Container = styled.div`
const FilterContainer = styled.div` const FilterContainer = styled.div`
display: flex; display: flex;
flex-wrap: wrap;
place-items: center; place-items: center;
gap: 0.5rem; gap: 0.5rem;
`; `;
export const languageBasedFilters = (field: string): Props["filterGroups"][number] =>
Object.entries(LanguageCodes).map(([key, value]) => ({
label: `${value}`,
filter: { where: { [field]: { not_equals: key } } },
}));
export const publishStatusFilters: Props["filterGroups"][number] = [
{ label: "Draft", filter: { where: { _status: { equals: "draft" } } } },
{ label: "Published", filter: { where: { _status: { equals: "published" } } } },
];

View File

@ -1,4 +1,27 @@
export enum CollectionGroup { export enum Collections {
ChronologyEras = "chronology-eras",
ChronologyItems = "chronology-items",
ContentsFolders = "contents-folders",
Contents = "contents",
ContentsThumbnails = "contents-thumbnails",
Currencies = "currencies",
Files = "files",
Keys = "keys",
Languages = "languages",
LibraryItems = "library-items",
LibraryItemsThumbnails = "library-items-thumbnails",
Posts = "posts",
PostsThumbnails = "posts-thumbnails",
Recorders = "recorders",
RecordersThumbnails = "recorders-thumbnails",
VideosChannels = "videos-channels",
Videos = "videos",
Weapons = "weapons",
WeaponsGroups = "weapons-groups",
WeaponsThumbnails = "weapons-thumbnails",
}
export enum CollectionGroups {
Collections = "Collections", Collections = "Collections",
Media = "Media", Media = "Media",
Meta = "Meta", Meta = "Meta",
@ -17,6 +40,15 @@ export enum KeysTypes {
Wordings = "Wordings", Wordings = "Wordings",
} }
export enum LanguageCodes {
en = "English",
fr = "French",
ja = "Japan",
es = "Spanish",
"pt-br" = "Portuguese",
"zh" = "Chinese",
}
export enum FileTypes { export enum FileTypes {
LibraryScans = "Library / Scans", LibraryScans = "Library / Scans",
LibrarySoundtracks = "Library / Soundtracks", LibrarySoundtracks = "Library / Soundtracks",
@ -51,3 +83,8 @@ export enum CollectionStatus {
Draft = "draft", Draft = "draft",
Published = "published", Published = "published",
} }
export enum VideoSources {
YouTube = "YouTube",
NicoNico = "NicoNico",
}

View File

@ -1,11 +1,12 @@
import payload from "payload"; import payload from "payload";
import { CollectionConfig } from "payload/types"; import { CollectionConfig } from "payload/types";
export const createBySlugEndpoint = <T>( export const createGetByEndpoint = <T, R>(
collection: string, collection: string,
handler: (doc: T) => unknown attribute: string,
handler: (doc: T) => Promise<R>
): CollectionConfig["endpoints"][number] => ({ ): CollectionConfig["endpoints"][number] => ({
path: "/slug/:slug", path: `/${attribute}/:${attribute}`,
method: "get", method: "get",
handler: async (req, res) => { handler: async (req, res) => {
if (!req.user) { if (!req.user) {
@ -20,13 +21,13 @@ export const createBySlugEndpoint = <T>(
const result = await payload.find({ const result = await payload.find({
collection, collection,
where: { slug: { equals: req.params.slug } }, where: { [attribute]: { equals: req.params[attribute] } },
}); });
if (result.docs.length === 0) { if (result.docs.length === 0) {
return res.sendStatus(404); return res.sendStatus(404);
} }
res.status(200).send(handler(result.docs[0])); res.status(200).send(await handler(result.docs[0]));
}, },
}); });

View File

@ -0,0 +1,99 @@
import payload from "payload";
import { CollectionConfig } from "payload/types";
import QueryString from "qs";
import { Recorder } from "../types/collections";
import { isDefined } from "../utils/asserts";
import { PayloadCreateData } from "../utils/types";
export const getAllStrapiEntries = async <T>(
collectionSlug: string,
params: Object
): Promise<T[]> => {
let page = 1;
let totalPage = 1;
const result: T[] = [];
while (page <= totalPage) {
const paramsWithPagination = QueryString.stringify({
...params,
pagination: { pageSize: 100, page },
});
const uri = `${process.env.STRAPI_URI}/api/${collectionSlug}?${paramsWithPagination}`;
const fetchResult = await fetch(uri, {
method: "GET",
headers: { authorization: `Bearer ${process.env.STRAPI_TOKEN}` },
});
const { data, meta } = await fetchResult.json();
result.push(...data);
totalPage = meta.pagination.pageCount;
page++;
}
return result;
};
type Params<T> = {
strapi: {
collection: string;
params: any;
};
payload: {
collection: string;
import?: (strapiObject: any, user: any) => Promise<void>;
convert?: (strapiObject: any) => PayloadCreateData<T>;
};
};
export const importStrapiEntries = async <T>({
strapi: strapiParams,
payload: payloadParams,
user,
}: Params<T> & { user: Recorder }) => {
const entries = await getAllStrapiEntries<any>(strapiParams.collection, strapiParams.params);
const errors = [];
await Promise.all(
entries.map(async ({ attributes, id }) => {
try {
if (isDefined(payloadParams.import)) {
await payloadParams.import(attributes, user);
} else if (isDefined(payloadParams.convert)) {
await payload.create({
collection: payloadParams.collection,
data: payloadParams.convert(attributes),
user,
});
} else {
throw new Error("No function was provided to handle importing the Strapi data");
}
} catch (e) {
console.warn(e);
errors.push(`${e.name} with ${id}`);
}
})
);
return { count: entries.length, errors };
};
export const createStrapiImportEndpoint = <T>(
params: Params<T>
): CollectionConfig["endpoints"][number] => ({
method: "get",
path: "/strapi",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const { count, errors } = await importStrapiEntries({ ...params, user: req.user });
res.status(200).json({ message: `${count} entries have been added successfully.`, errors });
},
});

View File

@ -0,0 +1,48 @@
import payload from "payload";
import { FieldBase } from "payload/dist/fields/config/types";
import { RelationshipField, Where } from "payload/types";
import { isNotEmpty } from "../../utils/asserts";
type BackPropagationField = FieldBase & {
where: (id: string) => Where;
relationTo: string;
hasMany: boolean;
};
export const backPropagationField = ({
admin,
hooks: { beforeChange = [], afterRead = [], ...otherHooks } = {},
where,
...params
}: BackPropagationField): RelationshipField => ({
...params,
type: "relationship",
admin: { ...admin, readOnly: true },
hooks: {
...otherHooks,
beforeChange: [
...beforeChange,
({ siblingData }) => {
delete siblingData[params.name];
},
],
afterRead: [
...afterRead,
async ({ data }) => {
if (isNotEmpty(data.id)) {
const result = await payload.find({
collection: params.relationTo,
where: where(data.id),
limit: 100,
depth: 0,
});
if (params.hasMany) {
return result.docs.map((doc) => doc.id);
} else {
return result.docs[0].id;
}
}
return params.hasMany ? [] : undefined;
},
],
},
});

View File

@ -1,10 +1,10 @@
import { RelationshipField, UploadField } from "payload/types"; import { RelationshipField, UploadField } from "payload/types";
import { Files } from "../../collections/Files/Files"; import { Collections } from "../../constants";
type Props = Omit<UploadField, "type" | "relationTo">; type Props = Omit<UploadField, "type" | "relationTo">;
export const fileField = (props: Props): RelationshipField => ({ export const fileField = (props: Props): RelationshipField => ({
...props, ...props,
type: "relationship", type: "relationship",
relationTo: Files.slug, relationTo: Collections.Files,
}); });

View File

@ -1,24 +1,32 @@
import { array } from "payload/dist/fields/validations";
import { ArrayField, Field } from "payload/types"; import { ArrayField, Field } from "payload/types";
import { hasDuplicates } from "../../utils/validation"; import { Collections } from "../../constants";
import { isDefined, isUndefined } from "../../utils/asserts"; import { isDefined, isUndefined } from "../../utils/asserts";
import { Languages } from "../../collections/Languages/Languages"; import { hasDuplicates } from "../../utils/validation";
import { RowLabel } from "./RowLabel";
import { Cell } from "./Cell"; import { Cell } from "./Cell";
import { RowLabel } from "./RowLabel";
const fieldsNames = { const fieldsNames = {
language: "language", language: "language",
sourceLanguage: "sourceLanguage", sourceLanguage: "sourceLanguage",
transcribers: "transcribers",
translators: "translators",
proofreaders: "proofreaders",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
type LocalizedFieldsProps = Omit<ArrayField, "type" | "admin"> & { type LocalizedFieldsProps = Omit<ArrayField, "type" | "admin" | "validate"> & {
admin?: ArrayField["admin"] & { useAsTitle?: string; hasSourceLanguage?: boolean }; admin?: ArrayField["admin"] & {
useAsTitle?: string;
hasSourceLanguage?: boolean;
hasCredits?: boolean;
};
}; };
type ArrayData = { [fieldsNames.language]?: string }[] | number | undefined; type ArrayData = { [fieldsNames.language]?: string }[] | number | undefined;
const languageField: Field = { const languageField: Field = {
name: fieldsNames.language, name: fieldsNames.language,
type: "relationship", type: "relationship",
relationTo: Languages.slug, relationTo: Collections.Languages,
required: true, required: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
}; };
@ -26,15 +34,72 @@ const languageField: Field = {
const sourceLanguageField: Field = { const sourceLanguageField: Field = {
name: fieldsNames.sourceLanguage, name: fieldsNames.sourceLanguage,
type: "relationship", type: "relationship",
relationTo: Languages.slug, relationTo: Collections.Languages,
required: true, required: true,
admin: { allowCreate: false, width: "50%" }, admin: { allowCreate: false, width: "50%" },
}; };
const creditFields: Field = {
type: "row",
admin: {
condition: (_, siblingData) =>
isDefined(siblingData.language) && isDefined(siblingData.sourceLanguage),
},
fields: [
{
name: fieldsNames.transcribers,
label: "Transcribers",
type: "relationship",
relationTo: "recorders",
hasMany: true,
admin: {
condition: (_, siblingData) => siblingData.language === siblingData.sourceLanguage,
width: "50%",
},
validate: (count, { siblingData }) => {
if (siblingData.language !== siblingData.sourceLanguage) {
return true;
}
if (isDefined(count) && count.length > 0) {
return true;
}
return "This field is required when the language is the same as the source language.";
},
},
{
name: fieldsNames.translators,
label: "Translators",
type: "relationship",
relationTo: "recorders",
hasMany: true,
admin: {
condition: (_, siblingData) => siblingData.language !== siblingData.sourceLanguage,
width: "50%",
},
validate: (count, { siblingData }) => {
if (siblingData.language === siblingData.sourceLanguage) {
return true;
}
if (isDefined(count) && count.length > 0) {
return true;
}
return "This field is required when the language is different from the source language.";
},
},
{
name: fieldsNames.proofreaders,
label: "Proofreaders",
type: "relationship",
relationTo: "recorders",
hasMany: true,
admin: { width: "50%" },
},
],
};
export const localizedFields = ({ export const localizedFields = ({
fields, fields,
validate, admin: { useAsTitle, hasSourceLanguage, hasCredits, ...admin } = {},
admin: { useAsTitle, hasSourceLanguage, ...admin },
...otherProps ...otherProps
}: LocalizedFieldsProps): ArrayField => ({ }: LocalizedFieldsProps): ArrayField => ({
...otherProps, ...otherProps,
@ -59,6 +124,9 @@ export const localizedFields = ({
...admin, ...admin,
}, },
validate: (value, options) => { validate: (value, options) => {
const defaultValidation = array(value, options);
if (defaultValidation !== true) return defaultValidation;
const data = options.data[otherProps.name] as ArrayData; const data = options.data[otherProps.name] as ArrayData;
if (isUndefined(data)) return true; if (isUndefined(data)) return true;
if (typeof data === "number") return true; if (typeof data === "number") return true;
@ -67,13 +135,12 @@ export const localizedFields = ({
if (hasDuplicates(languages)) { if (hasDuplicates(languages)) {
return `There cannot be multiple ${otherProps.name} with the same ${fieldsNames.language}`; return `There cannot be multiple ${otherProps.name} with the same ${fieldsNames.language}`;
} }
return isDefined(validate) ? validate(value, options) : true;
}, },
fields: [ fields: [
hasSourceLanguage hasSourceLanguage
? { type: "row", fields: [languageField, sourceLanguageField] } ? { type: "row", fields: [languageField, sourceLanguageField] }
: languageField, : languageField,
...fields, ...fields,
...(hasCredits ? [creditFields] : []),
], ],
}); });

View File

@ -1,25 +1,33 @@
import { buildConfig } from "payload/config";
import path from "path"; import path from "path";
import { Languages } from "./collections/Languages/Languages"; import { buildConfig } from "payload/config";
import { Recorders } from "./collections/Recorders/Recorders"; import { ChronologyEras } from "./collections/ChronologyEras/ChronologyEras";
import { Posts } from "./collections/Posts/Posts"; import { ChronologyItems } from "./collections/ChronologyItems/ChronologyItems";
import { Keys } from "./collections/Keys/Keys";
import { LibraryItems } from "./collections/LibraryItems/LibraryItems";
import { Contents } from "./collections/Contents/Contents"; import { Contents } from "./collections/Contents/Contents";
import { Files } from "./collections/Files/Files"; import { ContentsFolders } from "./collections/ContentsFolders/ContentsFolders";
import { RecorderThumbnails } from "./collections/RecorderThumbnails/RecorderThumbnails"; import { ContentsThumbnails } from "./collections/ContentsThumbnails/ContentsThumbnails";
import { PostThumbnails } from "./collections/PostThumbnails/PostThumbnails";
import { LibraryItemThumbnails } from "./collections/LibraryItemThumbnails/LibraryItemThumbnails";
import { ContentThumbnails } from "./collections/ContentThumbnails/ContentThumbnails";
import { ContentFolders } from "./collections/ContentFolders/ContentFolders";
import { Logo } from "./components/Logo";
import { Icon } from "./components/Icon";
import { Currencies } from "./collections/Currencies/Currencies"; import { Currencies } from "./collections/Currencies/Currencies";
import { Files } from "./collections/Files/Files";
import { Keys } from "./collections/Keys/Keys";
import { Languages } from "./collections/Languages/Languages";
import { LibraryItems } from "./collections/LibraryItems/LibraryItems";
import { LibraryItemsThumbnails } from "./collections/LibraryItemsThumbnails/LibraryItemsThumbnails";
import { Posts } from "./collections/Posts/Posts";
import { PostsThumbnails } from "./collections/PostsThumbnails/PostsThumbnails";
import { Recorders } from "./collections/Recorders/Recorders";
import { RecordersThumbnails } from "./collections/RecordersThumbnails/RecordersThumbnails";
import { Videos } from "./collections/Videos/Videos";
import { VideosChannels } from "./collections/VideosChannels/VideosChannels";
import { Weapons } from "./collections/Weapons/Weapons";
import { WeaponsGroups } from "./collections/WeaponsGroups/WeaponsGroups";
import { WeaponsThumbnails } from "./collections/WeaponsThumbnails/WeaponsThumbnails";
import { Icon } from "./components/Icon";
import { Logo } from "./components/Logo";
import { Collections } from "./constants";
export default buildConfig({ export default buildConfig({
serverURL: "https://dashboard.accords-library.com", serverURL: process.env.PAYLOAD_URI,
admin: { admin: {
user: Recorders.slug, user: Collections.Recorders,
components: { graphics: { Logo, Icon } }, components: { graphics: { Logo, Icon } },
meta: { meta: {
favicon: "/public/favicon.ico", favicon: "/public/favicon.ico",
@ -31,13 +39,20 @@ export default buildConfig({
collections: [ collections: [
LibraryItems, LibraryItems,
Contents, Contents,
ContentFolders, ContentsFolders,
Posts, Posts,
ContentThumbnails, ChronologyItems,
LibraryItemThumbnails, ChronologyEras,
RecorderThumbnails, Weapons,
PostThumbnails, WeaponsGroups,
WeaponsThumbnails,
ContentsThumbnails,
LibraryItemsThumbnails,
RecordersThumbnails,
PostsThumbnails,
Files, Files,
Videos,
VideosChannels,
Languages, Languages,
Currencies, Currencies,
Recorders, Recorders,

View File

@ -1,7 +1,10 @@
import express from "express";
import payload from "payload";
import "dotenv/config"; import "dotenv/config";
import express from "express";
import path from "path"; import path from "path";
import payload from "payload";
import { Collections, RecordersRoles } from "./constants";
import { Recorder } from "./types/collections";
import { PayloadCreateData } from "./utils/types";
const app = express(); const app = express();
@ -18,12 +21,34 @@ const start = async () => {
express: app, express: app,
onInit: async () => { onInit: async () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`); payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
const recorders = await payload.find({ collection: Collections.Recorders });
// If no recorders, we seed some initial data
if (recorders.docs.length === 0) {
payload.logger.info("Seeding some initial data");
const recorder: PayloadCreateData<Recorder> = {
email: process.env.SEEDING_ADMIN_EMAIL,
password: process.env.SEEDING_ADMIN_PASSWORD,
username: process.env.SEEDING_ADMIN_USERNAME,
role: [RecordersRoles.Admin],
anonymize: false,
};
await payload.create({
collection: Collections.Recorders,
data: recorder,
});
}
}, },
}); });
// Add your own express routes here // Add your own express routes here
app.use("/public", express.static(path.join(__dirname, "../public"))); app.use("/public", express.static(path.join(__dirname, "../public")));
app.get("/robots.txt", (_, res) => {
res.type("text/plain");
res.send("User-agent: *\nDisallow: /");
});
app.listen(process.env.PAYLOAD_PORT); app.listen(process.env.PAYLOAD_PORT);
}; };

View File

@ -27,13 +27,20 @@ export interface Config {
collections: { collections: {
"library-items": LibraryItem; "library-items": LibraryItem;
contents: Content; contents: Content;
"content-folders": ContentFolder; "contents-folders": ContentsFolder;
posts: Post; posts: Post;
"content-thumbnails": ContentThumbnail; "chronology-items": ChronologyItem;
"library-item-thumbnails": LibraryItemThumbnail; "chronology-eras": ChronologyEra;
"recorder-thumbnails": RecorderThumbnail; weapons: Weapon;
"post-thumbnails": PostThumbnail; "weapons-groups": WeaponsGroup;
"weapons-thumbnails": WeaponsThumbnail;
"contents-thumbnails": ContentsThumbnail;
"library-items-thumbnails": LibraryItemThumbnail;
"recorders-thumbnails": RecordersThumbnail;
"posts-thumbnails": PostThumbnail;
files: File; files: File;
videos: Video;
"videos-channels": VideosChannel;
languages: Language; languages: Language;
currencies: Currency; currencies: Currency;
recorders: Recorder; recorders: Recorder;
@ -65,7 +72,7 @@ export interface LibraryItem {
back?: string | LibraryItemThumbnail; back?: string | LibraryItemThumbnail;
id?: string; id?: string;
}[]; }[];
obibelt?: { obi?: {
front?: string | LibraryItemThumbnail; front?: string | LibraryItemThumbnail;
spine?: string | LibraryItemThumbnail; spine?: string | LibraryItemThumbnail;
back?: string | LibraryItemThumbnail; back?: string | LibraryItemThumbnail;
@ -124,6 +131,15 @@ export interface LibraryItem {
relationTo: "keys"; relationTo: "keys";
}[]; }[];
}; };
contents?: {
content: string | Content;
pageStart?: number;
pageEnd?: number;
timeStart?: number;
timeEnd?: number;
note?: string;
id?: string;
}[];
releaseDate?: string; releaseDate?: string;
updatedBy: string | Recorder; updatedBy: string | Recorder;
updatedAt: string; updatedAt: string;
@ -172,7 +188,7 @@ export interface Currency {
} }
export interface Key { export interface Key {
id: string; id: string;
slug: string; name: string;
type: type:
| "Contents" | "Contents"
| "LibraryAudio" | "LibraryAudio"
@ -190,56 +206,10 @@ export interface Language {
id: string; id: string;
name: string; name: string;
} }
export interface Recorder {
id: string;
username: string;
avatar?: string | RecorderThumbnail;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
role?: ("Admin" | "Recorder")[];
anonymize: boolean;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
hash?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}
export interface RecorderThumbnail {
id: string;
updatedAt: string;
createdAt: string;
url?: string;
filename?: string;
mimeType?: string;
filesize?: number;
width?: number;
height?: number;
sizes?: {
og?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
small?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
};
}
export interface Content { export interface Content {
id: string; id: string;
slug: string; slug: string;
thumbnail?: string | ContentThumbnail; thumbnail?: string | ContentsThumbnail;
categories?: categories?:
| { | {
value: string; value: string;
@ -275,7 +245,7 @@ export interface Content {
createdAt: string; createdAt: string;
_status?: "draft" | "published"; _status?: "draft" | "published";
} }
export interface ContentThumbnail { export interface ContentsThumbnail {
id: string; id: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@ -304,6 +274,53 @@ export interface ContentThumbnail {
}; };
}; };
} }
export interface Recorder {
id: string;
username: string;
avatar?: string | RecordersThumbnail;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
role?: ("Admin" | "Recorder")[];
anonymize: boolean;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
hash?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}
export interface RecordersThumbnail {
id: string;
recorder?: string | Recorder;
updatedAt: string;
createdAt: string;
url?: string;
filename?: string;
mimeType?: string;
filesize?: number;
width?: number;
height?: number;
sizes?: {
og?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
small?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
};
}
export interface TextBlock { export interface TextBlock {
content: { content: {
[k: string]: unknown; [k: string]: unknown;
@ -498,28 +515,12 @@ export interface File {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
export interface ContentFolder { export interface ContentsFolder {
id: string; id: string;
slug: string; slug: string;
translations?: ContentFoldersTranslation; translations?: ContentFoldersTranslation;
subfolders?: subfolders?: string[] | ContentsFolder[];
| { contents?: string[] | Content[];
value: string;
relationTo: "content-folders";
}[]
| {
value: ContentFolder;
relationTo: "content-folders";
}[];
contents?:
| {
value: string;
relationTo: "contents";
}[]
| {
value: Content;
relationTo: "contents";
}[];
} }
export interface Post { export interface Post {
id: string; id: string;
@ -591,3 +592,126 @@ export interface PostThumbnail {
}; };
}; };
} }
export interface ChronologyItem {
id: string;
name?: string;
date: {
year: number;
month?: number;
day?: number;
};
events?: {
translations?: {
language: string | Language;
title?: string;
description?: string;
notes?: string;
id?: string;
}[];
id?: string;
}[];
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: "draft" | "published";
}
export interface ChronologyEra {
id: string;
slug: string;
startingYear: number;
endingYear: number;
translations?: {
language: string | Language;
title: string;
description?: string;
id?: string;
}[];
updatedAt: string;
createdAt: string;
}
export interface Weapon {
id: string;
slug: string;
thumbnail?: string | WeaponsThumbnail;
type: string | Key;
group?: string | WeaponsGroup;
appearances: {
categories: string[] | Key[];
translations: {
language: string | Language;
sourceLanguage: string | Language;
name: string;
description?: string;
level1?: string;
level2?: string;
level3?: string;
level4?: string;
transcribers?: string[] | Recorder[];
translators?: string[] | Recorder[];
proofreaders?: string[] | Recorder[];
id?: string;
}[];
id?: string;
}[];
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: "draft" | "published";
}
export interface WeaponsThumbnail {
id: string;
updatedAt: string;
createdAt: string;
url?: string;
filename?: string;
mimeType?: string;
filesize?: number;
width?: number;
height?: number;
sizes?: {
og?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
small?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
};
}
export interface WeaponsGroup {
id: string;
slug: string;
translations?: {
language: string | Language;
name: string;
id?: string;
}[];
weapons?: string[] | Weapon[];
}
export interface Video {
id: string;
uid: string;
gone: boolean;
source: "YouTube" | "NicoNico";
title: string;
description?: string;
likes?: number;
views?: number;
publishedDate: string;
channel: string | VideosChannel;
}
export interface VideosChannel {
id: string;
uid: string;
title: string;
subscribers?: number;
}

View File

@ -5,3 +5,8 @@ export const isUndefined = <T>(value: T | null | undefined): value is null | und
!isDefined(value); !isDefined(value);
export const filterDefined = <T>(array: (T | null | undefined)[]): T[] => array.filter(isDefined); export const filterDefined = <T>(array: (T | null | undefined)[]): T[] => array.filter(isDefined);
export const isValidDate = (date: Date): boolean => date instanceof Date && !isNaN(date.getDate());
export const isNotEmpty = (value: string | null | undefined): value is string =>
isDefined(value) && value.trim().length > 0;

View File

@ -1,20 +1,19 @@
import { CollectionConfig } from "payload/types"; import { CollectionConfig } from "payload/types";
import slugify from "slugify"; import { Collections } from "../constants";
export type BuildCollectionConfig = Omit<CollectionConfig, "slug" | "typescript" | "labels">; export type BuildCollectionConfig = Omit<CollectionConfig, "slug" | "typescript" | "labels">;
export type GenerationFunctionProps = { export type GenerationFunctionProps = {
slug: string;
uploadDir: string; uploadDir: string;
}; };
export const buildCollectionConfig = ( export const buildCollectionConfig = (
slug: Collections,
labels: { singular: string; plural: string }, labels: { singular: string; plural: string },
generationFunction: (props: GenerationFunctionProps) => BuildCollectionConfig generationFunction: (props: GenerationFunctionProps) => BuildCollectionConfig
): CollectionConfig => { ): CollectionConfig => {
const slug = slugify(labels.plural, { lower: true, strict: true, trim: true });
const uploadDir = `../uploads/${slug}`; const uploadDir = `../uploads/${slug}`;
const config = generationFunction({ slug, uploadDir }); const config = generationFunction({ uploadDir });
return { return {
...config, ...config,
slug, slug,

59
src/utils/localApi.ts Normal file
View File

@ -0,0 +1,59 @@
import payload from "payload";
import { Collections, KeysTypes } from "../constants";
import { isDefined } from "./asserts";
export const findWeaponType = async (name: string): Promise<string> => {
const key = await payload.find({
collection: Collections.Keys,
where: { name: { equals: name }, type: { equals: KeysTypes.Weapons } },
});
return key.docs[0].id;
};
export const findCategory = async (name: string): Promise<string> => {
const key = await payload.find({
collection: Collections.Keys,
where: { name: { equals: name }, type: { equals: KeysTypes.Categories } },
});
return key.docs[0].id;
};
type UploadStrapiImage = {
image: StrapiImage;
collection: Collections;
};
type StrapiImage = {
data?: {
attributes: {
url: string;
mime: string;
name: string;
size: number;
};
};
};
export const uploadStrapiImage = async ({
collection,
image,
}: UploadStrapiImage): Promise<string> => {
if (isDefined(image.data)) {
const url = `${process.env.STRAPI_URI}${image.data.attributes.url}`;
const blob = await (await fetch(url)).blob();
const buffer = Buffer.from(await blob.arrayBuffer());
const result = await payload.create({
collection,
file: {
data: buffer,
mimetype: image.data.attributes.mime,
name: image.data.attributes.name,
size: image.data.attributes.size,
},
data: {},
});
return result.id;
}
};

View File

@ -1,7 +1,22 @@
import ISO6391 from "iso-639-1"; import tags from "language-tags";
export const shortenEllipsis = (text: string, length: number): string => export const shortenEllipsis = (text: string, length: number): string =>
text.length - 3 > length ? `${text.substring(0, length)}...` : text; text.length - 3 > length ? `${text.substring(0, length)}...` : text;
export const formatLanguageCode = (code: string): string => export const formatLanguageCode = (code: string): string =>
ISO6391.validate(code) ? ISO6391.getName(code) : code; tags(code).valid() ? tags(code).language().descriptions()[0] : code;
export const capitalize = (string: string): string => {
const [firstLetter, ...otherLetters] = string;
return [firstLetter.toUpperCase(), ...otherLetters].join("");
};
export const formatToCamelCase = (name: string): string =>
name
.toLowerCase()
.split(/[ \_-]/g)
.map((part, index) => (index > 0 ? capitalize(part) : part))
.join("");
export const formatToKebabCase = (name: string): string =>
name.toLowerCase().replaceAll(/[ \_]/g, "-");

4
src/utils/types.ts Normal file
View File

@ -0,0 +1,4 @@
export type PayloadCreateData<T> = Omit<
T,
"id" | "updatedAt" | "createdAt" | "sizes" | "updatedBy"
>;

View File

@ -1,10 +1,10 @@
import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types"; import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types";
import { Collections } from "../constants";
import { import {
BuildCollectionConfig, BuildCollectionConfig,
GenerationFunctionProps, GenerationFunctionProps,
buildCollectionConfig, buildCollectionConfig,
} from "./collectionConfig"; } from "./collectionConfig";
import { Recorders } from "../collections/Recorders/Recorders";
const fields = { updatedBy: "updatedBy" }; const fields = { updatedBy: "updatedBy" };
@ -17,13 +17,14 @@ const updatedByField = (): RelationshipField => ({
name: fields.updatedBy, name: fields.updatedBy,
type: "relationship", type: "relationship",
required: true, required: true,
relationTo: Recorders.slug, relationTo: Collections.Recorders,
admin: { readOnly: true, position: "sidebar" }, admin: { readOnly: true, position: "sidebar" },
}); });
type BuildVersionedCollectionConfig = Omit<BuildCollectionConfig, "timestamps" | "versions">; type BuildVersionedCollectionConfig = Omit<BuildCollectionConfig, "timestamps" | "versions">;
export const buildVersionedCollectionConfig = ( export const buildVersionedCollectionConfig = (
slug: Collections,
labels: { singular: string; plural: string }, labels: { singular: string; plural: string },
generationFunction: (props: GenerationFunctionProps) => BuildVersionedCollectionConfig generationFunction: (props: GenerationFunctionProps) => BuildVersionedCollectionConfig
): CollectionConfig => { ): CollectionConfig => {
@ -31,7 +32,7 @@ export const buildVersionedCollectionConfig = (
hooks: { beforeChange, ...otherHooks } = {}, hooks: { beforeChange, ...otherHooks } = {},
fields, fields,
...otherParams ...otherParams
} = buildCollectionConfig(labels, generationFunction); } = buildCollectionConfig(slug, labels, generationFunction);
return { return {
...otherParams, ...otherParams,