diff --git a/.env.example b/.env.example index 4952763..d8f07c7 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,13 @@ MONGODB_URI=mongodb://mongo:27017/payload MONGODB_PORT=27017 + +PAYLOAD_URI=https://payload.domain.com PAYLOAD_SECRET=payloadsecreta5e6ea45ef4e66eaa151612bdcb599df -PAYLOAD_PORT=3000 \ No newline at end of file +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 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d0c743c..e0ffbfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,8 @@ "css.lint.unknownAtRules": "ignore", "editor.rulers": [100], "typescript.preferences.importModuleSpecifier": "non-relative", -} \ No newline at end of file + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } +} diff --git a/package-lock.json b/package-lock.json index 1dd12f9..b02772e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,24 +9,29 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@fontsource/vollkorn": "^5.0.5", + "@fontsource/vollkorn": "^5.0.8", "clean-deep": "^3.4.0", "cross-env": "^7.0.3", "dotenv": "^16.3.1", "express": "^4.18.2", - "iso-639-1": "^2.1.15", - "payload": "^1.11.7", + "language-tags": "^1.0.8", + "luxon": "^3.4.0", + "payload": "^1.13.3", + "qs": "^6.11.2", "slugify": "^1.6.6", - "styled-components": "^6.0.5", + "styled-components": "^6.0.7", "unset-value": "^2.0.1" }, "devDependencies": { "@types/dotenv": "^8.2.0", "@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", "copyfiles": "^2.4.1", "nodemon": "^3.0.1", - "prettier": "^3.0.0", + "prettier": "^3.0.1", "ts-node": "^10.9.1", "typescript": "^5.1.6" } @@ -3687,9 +3692,9 @@ } }, "node_modules/@fontsource/vollkorn": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@fontsource/vollkorn/-/vollkorn-5.0.5.tgz", - "integrity": "sha512-8YQT9VLc6z0rxuyrDRLrcQnYFJ8eZOOKs4Oat7HUMHYAwRe20XK1khUkiTLb5jGXD4XDfAwFW9sV21SJoyUhHQ==" + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@fontsource/vollkorn/-/vollkorn-5.0.8.tgz", + "integrity": "sha512-QSPfmwObfsqSNMJlDWeixrycNiOtTh8VrCP1khT1u3wUhESHAgj+FVcyB9IdWj9Z1jTWc1fZ9atPunSovze0YA==" }, "node_modules/@hapi/hoek": { "version": "9.3.0", @@ -4375,9 +4380,9 @@ } }, "node_modules/@swc/core": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.68.tgz", - "integrity": "sha512-njGQuJO+Wy06dEayt70cf0c/KI3HGjm4iW9LLViVLBuYNzJ4SSdNfzejludzufu6im+dsDJ0i3QjgWhAIcVHMQ==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.75.tgz", + "integrity": "sha512-YLqd5oZVnaOq/OzkjRSsJUQqAfKYiD0fzUyVUPVlNNCoQEfVfSMcXH80hLmYe9aDH0T/a7qEMjWyIr/0kWqy1A==", "hasInstallScript": true, "engines": { "node": ">=10" @@ -4387,16 +4392,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.68", - "@swc/core-darwin-x64": "1.3.68", - "@swc/core-linux-arm-gnueabihf": "1.3.68", - "@swc/core-linux-arm64-gnu": "1.3.68", - "@swc/core-linux-arm64-musl": "1.3.68", - "@swc/core-linux-x64-gnu": "1.3.68", - "@swc/core-linux-x64-musl": "1.3.68", - "@swc/core-win32-arm64-msvc": "1.3.68", - "@swc/core-win32-ia32-msvc": "1.3.68", - "@swc/core-win32-x64-msvc": "1.3.68" + "@swc/core-darwin-arm64": "1.3.75", + "@swc/core-darwin-x64": "1.3.75", + "@swc/core-linux-arm-gnueabihf": "1.3.75", + "@swc/core-linux-arm64-gnu": "1.3.75", + "@swc/core-linux-arm64-musl": "1.3.75", + "@swc/core-linux-x64-gnu": "1.3.75", + "@swc/core-linux-x64-musl": "1.3.75", + "@swc/core-win32-arm64-msvc": "1.3.75", + "@swc/core-win32-ia32-msvc": "1.3.75", + "@swc/core-win32-x64-msvc": "1.3.75" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -4408,9 +4413,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.68.tgz", - "integrity": "sha512-Z5pNxeuP2NxpOHTzDQkJs0wAPLnTlglZnR3WjObijwvdwT/kw1Y5EPDKM/BVSIeG40SPMkDLBbI0aj0qyXzrBA==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.75.tgz", + "integrity": "sha512-anDnx9L465lGbjB2mvcV54NGHW6illr0IDvVV7JmkabYUVneaRdQvTr0tbHv3xjHnjrK1wuwVOHKV0LcQF2tnQ==", "cpu": [ "arm64" ], @@ -4423,9 +4428,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.68.tgz", - "integrity": "sha512-ZHl42g6yXhfX4PzAQ0BNvBXpt/OcbAHfubWRN6eXELK3fiNnxL7QBW1if7iizlq6iA+Mj1pwHyyUit1pz0+fgA==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.75.tgz", + "integrity": "sha512-dIHDfrLmeZfr2xwi1whO7AmzdI3HdamgvxthaL+S8L1x8TeczAZEvsmZTjy3s8p3Va4rbGXcb3+uBhmfkqCbfw==", "cpu": [ "x64" ], @@ -4438,9 +4443,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.68.tgz", - "integrity": "sha512-Mk8f6KCOQ2CNAR4PtWajIjS6XKSSR7ZYDOCf1GXRxhS3qEyQH7V8elWvqWYqHcT4foO60NUmxA/NOM/dQrdO1A==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.75.tgz", + "integrity": "sha512-qeJmvMGrjC6xt+G0R4kVqqxvlhxJx7tTzhcEoWgLJnfvGZiF6SJdsef4OSM7HuReXrlBoEtJbfGPrLJtbV+C0w==", "cpu": [ "arm" ], @@ -4453,9 +4458,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.68.tgz", - "integrity": "sha512-RhBllggh9t9sIxaRgRcGrVaS7fDk6KsIqR6b9+dwU5OyDr4ZyHWw1ZaH/1/HAebuXYhNBjoNUiRtca6lKRIPgQ==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.75.tgz", + "integrity": "sha512-sqA9JqHEJBF4AdNuwo5zRqq0HC3l31SPsG9zpRa4nRzG5daBBJ80H7fi6PZQud1rfNNq+Q08gjYrdrxwHstvjw==", "cpu": [ "arm64" ], @@ -4468,9 +4473,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.68.tgz", - "integrity": "sha512-8K3zjU+tFgn6yGDEeD343gkKaHU9dhz77NiVkI1VzwRaT/Ag5pwl5eMQ1yStm8koNFzn3zq6rGjHfI5g2yI5Wg==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.75.tgz", + "integrity": "sha512-95rQT5xTAL3eKhMJbJbLsZHHP9EUlh1rcrFoLf0gUApoVF8g94QjZ9hYZiI72mMP5WPjgTEXQVnVB9O2GxeaLw==", "cpu": [ "arm64" ], @@ -4483,9 +4488,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.68.tgz", - "integrity": "sha512-4xAnvsBOyeTL0AB8GWlRKDM/hsysJ5jr5qvdKKI3rZfJgnnxl/xSX6TJKPsJ8gygfUJ3BmfCbmUmEyeDZ3YPvA==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.75.tgz", + "integrity": "sha512-If7UpAhnPduMmtC+TSgPpZ1UXZfp2hIpjUFxpeCmHHYLS6Fn/2GZC5hpEiu+wvFJF0hzPh93eNAHa9gUxGUG+w==", "cpu": [ "x64" ], @@ -4498,9 +4503,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.68.tgz", - "integrity": "sha512-RCpaBo1fcpy1EFdjF+I7N4lfzOaHXVV0iMw/ABM+0PD6tp3V/9pxsguaZyeAHyEiUlDA6PZ4TfXv5zfnXEgW4Q==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.75.tgz", + "integrity": "sha512-HOhxX0YNHTElCZqIviquka3CGYTN8rSQ6BdFfSk/K0O+ZEHx3qGte0qr+gGLPF/237GxreUkp3OMaWKuURtuCg==", "cpu": [ "x64" ], @@ -4513,9 +4518,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.68.tgz", - "integrity": "sha512-v2WZvXrSslYEpY1nqpItyamL4DyaJinmOkXvM8Bc1LLKU5rGuvmBdjUYg/5Y+o0AUynuiWubpgHNOkBWiCvfqw==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.75.tgz", + "integrity": "sha512-7QPI+mvBXAerVfWahrgBNe+g7fK8PuetxFnZSEmXUcDXvWcdJXAndD7GjAJzbDyjQpLKHbsDKMiHYvfNxZoN/A==", "cpu": [ "arm64" ], @@ -4528,9 +4533,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.68.tgz", - "integrity": "sha512-HH5NJrIdzkJs+1xxprie0qSCMBeL9yeEhcC1yZTzYv8bwmabOUSdtKIqS55iYP/2hLWn9CTbvKPmLOIhCopW3Q==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.75.tgz", + "integrity": "sha512-EfABCy4Wlq7O5ShWsm32FgDkSjyeyj/SQ4wnUIvWpkXhgfT1iNXky7KRU1HtX+SmnVk/k/NnabVZpIklYbjtZA==", "cpu": [ "ia32" ], @@ -4543,9 +4548,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.68", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.68.tgz", - "integrity": "sha512-9HZVtLQUgK8r/yXQdwe0VBexbIcrY6+fBROhs7AAPWdewpaUeLkwQEJk6TbYr9CQuHw26FFGg6SjwAiqXF+kgQ==", + "version": "1.3.75", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.75.tgz", + "integrity": "sha512-cTvP0pOD9C3pSp1cwtt85ZsrUkQz8RZfSPhM+jCGxKxmoowDCnInoOQ4Ica/ehyuUnQ4/IstSdYtYpO5yzPDJg==", "cpu": [ "x64" ], @@ -4743,11 +4748,23 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "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": { "version": "4.14.195", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -5474,6 +5491,20 @@ "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": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", @@ -6034,6 +6065,14 @@ "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": { "version": "0.5.4", "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", "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": { "version": "1.7.0", "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", "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": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -8760,6 +8805,19 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -8888,6 +8946,14 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -9251,23 +9317,6 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -9848,9 +9897,9 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/payload": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/payload/-/payload-1.11.7.tgz", - "integrity": "sha512-pVImP0b8MA9VfcXDITqq8KlIVaFB81FMtt24TPrBfNqkG53cRFxnYiGRJS40p/CNVknCpSRGVBkdpKmVNQ07YQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/payload/-/payload-1.13.4.tgz", + "integrity": "sha512-toVqxxxq4SJwBJtmCLaLOReza0JSidCrH4jTBxcy4h/qOYyeCAS4wApUFYhDI7JhB3KCd2+jbpVbhHOU4ALBKA==", "dependencies": { "@date-io/date-fns": "^2.16.0", "@dnd-kit/core": "^6.0.7", @@ -9859,7 +9908,7 @@ "@faceless-ui/scroll-info": "^1.3.0", "@faceless-ui/window-info": "^2.1.1", "@monaco-editor/react": "^4.5.1", - "@swc/core": "^1.3.26", + "@swc/core": "1.3.75", "@swc/register": "^0.1.10", "@types/sharp": "^0.31.1", "body-parser": "^1.20.1", @@ -9867,6 +9916,7 @@ "compression": "^1.7.4", "conf": "^10.2.0", "connect-history-api-fallback": "^1.6.0", + "console-table-printer": "^2.11.2", "css-loader": "^5.2.7", "css-minimizer-webpack-plugin": "^5.0.0", "dataloader": "^2.1.0", @@ -11386,6 +11436,23 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "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": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -11412,9 +11479,9 @@ } }, "node_modules/prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", - "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz", + "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -11510,9 +11577,9 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, @@ -12488,6 +12555,11 @@ "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": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -12782,9 +12854,9 @@ } }, "node_modules/styled-components": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.0.5.tgz", - "integrity": "sha512-308zi5o7LrA9cVaP4nPD0TaUpOjGPePkAUFb/OGB0xRI3I9ozpW5UyASvRVi9wJcYASG+Y3mLDLDUZC7nqzimw==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.0.7.tgz", + "integrity": "sha512-xIwWuiRMYR43mskVsW9MGTRjSo7ol4bcVjT595fGUp3OLBJOlOgaiKaxsHdC4a2HqWKqKnh0CmcRbk5ogyDjTg==", "dependencies": { "@babel/cli": "^7.21.0", "@babel/core": "^7.21.0", diff --git a/package.json b/package.json index d5e735e..488ae25 100644 --- a/package.json +++ b/package.json @@ -16,27 +16,34 @@ "prettier": "prettier --list-different --end-of-line auto --write src", "tsc": "tsc --noEmit", "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": { - "@fontsource/vollkorn": "^5.0.5", + "@fontsource/vollkorn": "^5.0.8", "clean-deep": "^3.4.0", "cross-env": "^7.0.3", "dotenv": "^16.3.1", "express": "^4.18.2", - "iso-639-1": "^2.1.15", - "payload": "^1.11.7", + "language-tags": "^1.0.8", + "luxon": "^3.4.0", + "payload": "^1.13.3", + "qs": "^6.11.2", "slugify": "^1.6.6", - "styled-components": "^6.0.5", + "styled-components": "^6.0.7", "unset-value": "^2.0.1" }, "devDependencies": { "@types/dotenv": "^8.2.0", "@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", "copyfiles": "^2.4.1", "nodemon": "^3.0.1", - "prettier": "^3.0.0", + "prettier": "^3.0.1", "ts-node": "^10.9.1", "typescript": "^5.1.6" } diff --git a/src/accesses/publicAccess.ts b/src/accesses/publicAccess.ts new file mode 100644 index 0000000..8ed82c9 --- /dev/null +++ b/src/accesses/publicAccess.ts @@ -0,0 +1 @@ +export const publicAccess = () => true; diff --git a/src/collections/ChronologyEras/ChronologyEras.ts b/src/collections/ChronologyEras/ChronologyEras.ts new file mode 100644 index 0000000..ce2e078 --- /dev/null +++ b/src/collections/ChronologyEras/ChronologyEras.ts @@ -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; + +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", + }, + ], + }), + ], + }) +); diff --git a/src/collections/ChronologyEras/endpoints/importFromStrapi.ts b/src/collections/ChronologyEras/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..8737f88 --- /dev/null +++ b/src/collections/ChronologyEras/endpoints/importFromStrapi.ts @@ -0,0 +1,25 @@ +import { Collections } from "../../../constants"; +import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint"; +import { ChronologyEra } from "../../../types/collections"; + +export const importFromStrapi = createStrapiImportEndpoint({ + 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, + })), + }), + }, +}); diff --git a/src/collections/ChronologyItems/ChronologyItems.ts b/src/collections/ChronologyItems/ChronologyItems.ts new file mode 100644 index 0000000..44a518f --- /dev/null +++ b/src/collections/ChronologyItems/ChronologyItems.ts @@ -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; + +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" }, + ], + }), + ], + }, + ], + }) +); diff --git a/src/collections/ChronologyItems/endpoints/importFromStrapi.ts b/src/collections/ChronologyItems/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..dfc44ca --- /dev/null +++ b/src/collections/ChronologyItems/endpoints/importFromStrapi.ts @@ -0,0 +1,26 @@ +import { Collections } from "../../../constants"; +import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint"; +import { ChronologyItem } from "../../../types/collections"; + +export const importFromStrapi = createStrapiImportEndpoint({ + 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, + })), + })), + }), + }, +}); diff --git a/src/collections/Contents/Contents.ts b/src/collections/Contents/Contents.ts index 647b325..8a9e4b7 100644 --- a/src/collections/Contents/Contents.ts +++ b/src/collections/Contents/Contents.ts @@ -1,17 +1,14 @@ -import { CollectionGroup, 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 { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants"; import { fileField } from "../../fields/fileField/fileField"; -import { contentBlocks } from "./Blocks/blocks"; -import { ContentThumbnails } from "../ContentThumbnails/ContentThumbnails"; -import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; +import { imageField } from "../../fields/imageField/imageField"; +import { slugField } from "../../fields/slugField/slugField"; +import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; 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 = { slug: "slug", @@ -37,6 +34,7 @@ const fields = { } as const satisfies Record; export const Contents = buildVersionedCollectionConfig( + Collections.Contents, { singular: "Content", plural: "Contents", @@ -55,7 +53,7 @@ export const Contents = buildVersionedCollectionConfig( fields.translations, fields.status, ], - group: CollectionGroup.Collections, + group: CollectionGroups.Collections, hooks: { beforeDuplicate: beforeDuplicatePiping([ beforeDuplicateUnpublish, @@ -71,7 +69,7 @@ export const Contents = buildVersionedCollectionConfig( slugField({ name: fields.slug, admin: { width: "50%" } }), imageField({ name: fields.thumbnail, - relationTo: ContentThumbnails.slug, + relationTo: Collections.ContentsThumbnails, admin: { width: "50%" }, }), ], @@ -82,7 +80,7 @@ export const Contents = buildVersionedCollectionConfig( { name: fields.categories, type: "relationship", - relationTo: [Keys.slug], + relationTo: [Collections.Keys], filterOptions: { type: { equals: KeysTypes.Categories } }, hasMany: true, admin: { allowCreate: false, width: "50%" }, @@ -90,7 +88,7 @@ export const Contents = buildVersionedCollectionConfig( { name: fields.type, type: "relationship", - relationTo: [Keys.slug], + relationTo: [Collections.Keys], filterOptions: { type: { equals: KeysTypes.Contents } }, admin: { allowCreate: false, width: "50%" }, }, @@ -128,7 +126,7 @@ export const Contents = buildVersionedCollectionConfig( name: fields.textTranscribers, label: "Transcribers", type: "relationship", - relationTo: Recorders.slug, + relationTo: Collections.Recorders, hasMany: true, admin: { condition: (_, siblingData) => @@ -140,7 +138,7 @@ export const Contents = buildVersionedCollectionConfig( name: fields.textTranslators, label: "Translators", type: "relationship", - relationTo: Recorders.slug, + relationTo: Collections.Recorders, hasMany: true, admin: { condition: (_, siblingData) => @@ -152,7 +150,7 @@ export const Contents = buildVersionedCollectionConfig( name: fields.textProofreaders, label: "Proofreaders", type: "relationship", - relationTo: Recorders.slug, + relationTo: Collections.Recorders, hasMany: true, admin: { width: "50%" }, }, diff --git a/src/collections/ContentFolders/ContentFolders.ts b/src/collections/ContentsFolders/ContentsFolders.ts similarity index 78% rename from src/collections/ContentFolders/ContentFolders.ts rename to src/collections/ContentsFolders/ContentsFolders.ts index 9cbc83e..8347f81 100644 --- a/src/collections/ContentFolders/ContentFolders.ts +++ b/src/collections/ContentsFolders/ContentsFolders.ts @@ -1,7 +1,6 @@ +import { CollectionGroups, Collections } from "../../constants"; import { slugField } from "../../fields/slugField/slugField"; -import { CollectionGroup } from "../../constants"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; -import { Contents } from "../Contents/Contents"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -12,18 +11,19 @@ const fields = { contents: "contents", } as const satisfies Record; -export const ContentFolders = buildCollectionConfig( +export const ContentsFolders = buildCollectionConfig( + Collections.ContentsFolders, { - singular: "Content Folder", - plural: "Content Folders", + singular: "Contents Folder", + plural: "Contents Folders", }, - ({ slug }) => ({ + () => ({ defaultSort: fields.slug, admin: { useAsTitle: fields.slug, defaultColumns: [fields.slug, fields.translations], disableDuplicate: true, - group: CollectionGroup.Collections, + group: CollectionGroups.Collections, }, timestamps: false, versions: false, @@ -43,14 +43,14 @@ export const ContentFolders = buildCollectionConfig( { type: "relationship", name: fields.subfolders, - relationTo: [slug], + relationTo: Collections.ContentsFolders, hasMany: true, admin: { width: "50%" }, }, { type: "relationship", name: fields.contents, - relationTo: [Contents.slug], + relationTo: Collections.Contents, hasMany: true, admin: { width: "50%" }, }, diff --git a/src/collections/ContentThumbnails/ContentThumbnails.ts b/src/collections/ContentsThumbnails/ContentsThumbnails.ts similarity index 78% rename from src/collections/ContentThumbnails/ContentThumbnails.ts rename to src/collections/ContentsThumbnails/ContentsThumbnails.ts index bb9e95c..682a429 100644 --- a/src/collections/ContentThumbnails/ContentThumbnails.ts +++ b/src/collections/ContentsThumbnails/ContentsThumbnails.ts @@ -1,4 +1,4 @@ -import { CollectionGroup } from "../../constants"; +import { CollectionGroups, Collections } from "../../constants"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -7,17 +7,18 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const ContentThumbnails = buildCollectionConfig( +export const ContentsThumbnails = buildCollectionConfig( + Collections.ContentsThumbnails, { - singular: "Content Thumbnail", - plural: "Content Thumbnails", + singular: "Contents Thumbnail", + plural: "Contents Thumbnails", }, ({ uploadDir }) => ({ defaultSort: fields.filename, admin: { useAsTitle: fields.filename, disableDuplicate: true, - group: CollectionGroup.Media, + group: CollectionGroups.Media, }, upload: { staticDir: uploadDir, diff --git a/src/collections/Currencies/Currencies.ts b/src/collections/Currencies/Currencies.ts index 78dbed3..1d6a484 100644 --- a/src/collections/Currencies/Currencies.ts +++ b/src/collections/Currencies/Currencies.ts @@ -1,12 +1,15 @@ +import { text } from "payload/dist/fields/validations"; import { mustBeAdmin } from "../../accesses/mustBeAdmin"; -import { CollectionGroup } from "../../constants"; +import { CollectionGroups, Collections } from "../../constants"; import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { importFromStrapi } from "./endpoints/importFromStrapi"; const fields = { id: "id", } as const satisfies Record; export const Currencies = buildCollectionConfig( + Collections.Currencies, { singular: "Currency", plural: "Currencies", @@ -14,12 +17,14 @@ export const Currencies = buildCollectionConfig( () => ({ defaultSort: fields.id, admin: { + pagination: { defaultLimit: 100 }, useAsTitle: fields.id, defaultColumns: [fields.id], disableDuplicate: true, - group: CollectionGroup.Meta, + group: CollectionGroups.Meta, }, access: { create: mustBeAdmin, update: mustBeAdmin }, + endpoints: [importFromStrapi], timestamps: false, fields: [ { @@ -27,11 +32,11 @@ export const Currencies = buildCollectionConfig( type: "text", unique: true, required: true, - validate: (value) => { - if (/^[A-Z]{3}$/g.test(value)) { - return true; + validate: (value, options) => { + if (!/^[A-Z]{3}$/g.test(value)) { + 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); }, }, ], diff --git a/src/collections/Currencies/endpoints/importFromStrapi.ts b/src/collections/Currencies/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..d488070 --- /dev/null +++ b/src/collections/Currencies/endpoints/importFromStrapi.ts @@ -0,0 +1,14 @@ +import { Collections } from "../../../constants"; +import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint"; +import { Language } from "../../../types/collections"; + +export const importFromStrapi = createStrapiImportEndpoint({ + strapi: { + collection: "currencies", + params: {}, + }, + payload: { + collection: Collections.Currencies, + convert: ({ code, name }) => ({ id: code, name }), + }, +}); diff --git a/src/collections/Files/Files.ts b/src/collections/Files/Files.ts index d3749b6..4046cab 100644 --- a/src/collections/Files/Files.ts +++ b/src/collections/Files/Files.ts @@ -1,4 +1,4 @@ -import { CollectionGroup, FileTypes } from "../../constants"; +import { CollectionGroups, Collections, FileTypes } from "../../constants"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -7,6 +7,7 @@ const fields = { } as const satisfies Record; export const Files = buildCollectionConfig( + Collections.Files, { singular: "File", plural: "Files", @@ -16,7 +17,7 @@ export const Files = buildCollectionConfig( admin: { useAsTitle: fields.filename, disableDuplicate: true, - group: CollectionGroup.Media, + group: CollectionGroups.Media, }, fields: [ { diff --git a/src/collections/Keys/Keys.ts b/src/collections/Keys/Keys.ts index 79b559d..c9a6824 100644 --- a/src/collections/Keys/Keys.ts +++ b/src/collections/Keys/Keys.ts @@ -1,60 +1,86 @@ -import { CollectionConfig } from "payload/types"; -import { slugField } from "../../fields/slugField/slugField"; -import { CollectionGroup, KeysTypes } from "../../constants"; +import payload from "payload"; +import { mustBeAdmin } from "../../accesses/mustBeAdmin"; +import { QuickFilters } from "../../components/QuickFilters"; +import { CollectionGroups, Collections, KeysTypes, LanguageCodes } from "../../constants"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { Key } from "../../types/collections"; import { isDefined } from "../../utils/asserts"; import { buildCollectionConfig } from "../../utils/collectionConfig"; -import { mustBeAdmin } from "../../accesses/mustBeAdmin"; -import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; -import { QuickFilters } from "../../components/QuickFilters"; +import { importFromStrapi } from "./endpoints/importFromStrapi"; const fields = { - slug: "slug", - translations: "translations", - type: "type", name: "name", - short: "short", + type: "type", + translations: "translations", + translationsName: "name", + translationsShort: "short", } as const satisfies Record; const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"]; -export const Keys: CollectionConfig = buildCollectionConfig( +export const Keys = buildCollectionConfig( + Collections.Keys, { singular: "Key", plural: "Keys", }, () => ({ - defaultSort: fields.slug, + defaultSort: fields.name, admin: { - useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.type, fields.translations], - group: CollectionGroup.Meta, + useAsTitle: fields.name, + defaultColumns: [fields.name, fields.type, fields.translations], + group: CollectionGroups.Meta, components: { BeforeListTable: [ () => QuickFilters({ - route: "/admin/collections/keys", - filters: [ - { label: "Wordings", filter: "where[type][equals]=Wordings" }, - { label: "∅ English", filter: "where[translations.language][not_equals]=en" }, - { label: "∅ French", filter: "where[translations.language][not_equals]=fr" }, + slug: Collections.Keys, + filterGroups: [ + Object.entries(KeysTypes).map(([key, value]) => ({ + label: value, + filter: { where: { type: { equals: key } } }, + })), + Object.entries(LanguageCodes).map(([key, value]) => ({ + label: `∅ ${value}`, + filter: { where: { "translations.language": { not_equals: key } } }, + })), ], }), ], }, hooks: { - beforeDuplicate: beforeDuplicateAddCopyTo(fields.slug), + beforeDuplicate: beforeDuplicateAddCopyTo(fields.name), }, }, access: { create: 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, versions: false, fields: [ - slugField({ name: fields.slug }), + { + name: fields.name, + type: "text", + required: true, + }, { name: fields.type, type: "select", @@ -65,15 +91,20 @@ export const Keys: CollectionConfig = buildCollectionConfig( name: fields.translations, interfaceName: "CategoryTranslations", admin: { - useAsTitle: fields.name, + useAsTitle: fields.translationsName, }, fields: [ { type: "row", 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", admin: { condition: (data: Partial) => diff --git a/src/collections/Keys/endpoints/importFromStrapi.ts b/src/collections/Keys/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..b08556a --- /dev/null +++ b/src/collections/Keys/endpoints/importFromStrapi.ts @@ -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(strapiParams.collection, strapiParams.params); + + const { ui_language, createdAt, updatedAt, ...otherKeys } = rawEntries[0].attributes; + + const entries: PayloadCreateData[] = 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({ + 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( + { + 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({ + 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({ + 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( + { + 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( + { + 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({ + 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( + { + 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({ + 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, + }, + }); + }, +}; diff --git a/src/collections/Languages/Languages.ts b/src/collections/Languages/Languages.ts index fc35515..fc7556c 100644 --- a/src/collections/Languages/Languages.ts +++ b/src/collections/Languages/Languages.ts @@ -1,6 +1,9 @@ +import { text } from "payload/dist/fields/validations"; 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 { importFromStrapi } from "./endpoints/importFromStrapi"; const fields = { id: "id", @@ -8,6 +11,7 @@ const fields = { } as const satisfies Record; export const Languages = buildCollectionConfig( + Collections.Languages, { singular: "Language", plural: "Languages", @@ -18,21 +22,23 @@ export const Languages = buildCollectionConfig( useAsTitle: fields.name, defaultColumns: [fields.name, fields.id], 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, + endpoints: [importFromStrapi], fields: [ { name: fields.id, type: "text", unique: true, required: true, - validate: (value) => { - if (/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { - return true; + validate: (value, options) => { + if (!/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { + 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); }, }, { diff --git a/src/collections/Languages/endpoints/importFromStrapi.ts b/src/collections/Languages/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..0864fba --- /dev/null +++ b/src/collections/Languages/endpoints/importFromStrapi.ts @@ -0,0 +1,14 @@ +import { Collections } from "../../../constants"; +import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint"; +import { Language } from "../../../types/collections"; + +export const importFromStrapi = createStrapiImportEndpoint({ + strapi: { + collection: "languages", + params: {}, + }, + payload: { + collection: Collections.Languages, + convert: ({ code, name }) => ({ id: code, name }), + }, +}); diff --git a/src/collections/LibraryItems/LibraryItems.ts b/src/collections/LibraryItems/LibraryItems.ts index 6fdde93..4c9ebe9 100644 --- a/src/collections/LibraryItems/LibraryItems.ts +++ b/src/collections/LibraryItems/LibraryItems.ts @@ -1,24 +1,21 @@ import { - CollectionGroup, + CollectionGroups, + Collections, KeysTypes, LibraryItemsTextualBindingTypes, LibraryItemsTextualPageOrders, LibraryItemsTypes, } from "../../constants"; -import { slugField } from "../../fields/slugField/slugField"; 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 { 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 { getSlug } from "./endpoints/getSlug"; +import { getBySlug } from "./endpoints/getBySlug"; const fields = { status: "status", @@ -57,21 +54,29 @@ const fields = { scansDustjacketFront: "front", scansDustjacketSpine: "spine", scansDustjacketBack: "back", - scansObibelt: "obibelt", - scansObibeltFront: "front", - scansObibeltSpine: "spine", - scansObibeltBack: "back", + scansObi: "obi", + scansObiFront: "front", + scansObiSpine: "spine", + scansObiBack: "back", scansPages: "pages", scansPagesPage: "page", scansPagesImage: "image", + contents: "contents", + contentsContent: "content", + contentsPageStart: "pageStart", + contentsPageEnd: "pageEnd", + contentsTimeStart: "timeStart", + contentsTimeEnd: "timeEnd", + contentsNote: "note", } as const satisfies Record; export const LibraryItems = buildVersionedCollectionConfig( + Collections.LibraryItems, { singular: "Library Item", plural: "Library Items", }, - ({ slug }) => ({ + () => ({ defaultSort: fields.slug, admin: { useAsTitle: fields.slug, @@ -79,7 +84,7 @@ export const LibraryItems = buildVersionedCollectionConfig( "A comprehensive list of all Yokoverse’s side materials (books, novellas, artbooks, \ stage plays, manga, drama CDs, and comics).", defaultColumns: [fields.slug, fields.thumbnail, fields.status], - group: CollectionGroup.Collections, + group: CollectionGroups.Collections, hooks: { beforeDuplicate: beforeDuplicatePiping([ beforeDuplicateUnpublish, @@ -88,7 +93,7 @@ export const LibraryItems = buildVersionedCollectionConfig( }, preview: (doc) => `https://accords-library.com/library/${doc.slug}`, }, - endpoints: [getSlug(slug)], + endpoints: [getBySlug], fields: [ { type: "row", @@ -96,7 +101,7 @@ export const LibraryItems = buildVersionedCollectionConfig( slugField({ name: fields.slug, admin: { width: "50%" } }), imageField({ name: fields.thumbnail, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "50%" }, }), ], @@ -167,17 +172,17 @@ export const LibraryItems = buildVersionedCollectionConfig( fields: [ imageField({ name: fields.scansCoverFront, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), imageField({ name: fields.scansCoverSpine, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), imageField({ name: fields.scansCoverBack, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), ], @@ -188,23 +193,28 @@ export const LibraryItems = buildVersionedCollectionConfig( name: fields.scansDustjacket, label: "Dust Jacket", 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: [ { type: "row", fields: [ imageField({ name: fields.scansDustjacketFront, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), imageField({ name: fields.scansDustjacketSpine, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), imageField({ name: fields.scansDustjacketBack, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), ], @@ -212,26 +222,31 @@ export const LibraryItems = buildVersionedCollectionConfig( ], }), optionalGroupField({ - name: fields.scansObibelt, - label: "Obi Belt", + name: fields.scansObi, + label: "Obi", 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: [ { type: "row", fields: [ imageField({ - name: fields.scansObibeltFront, - relationTo: LibraryItemThumbnails.slug, + name: fields.scansObiFront, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), imageField({ - name: fields.scansObibeltSpine, - relationTo: LibraryItemThumbnails.slug, + name: fields.scansObiSpine, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), imageField({ - name: fields.scansObibeltBack, - relationTo: LibraryItemThumbnails.slug, + name: fields.scansObiBack, + relationTo: Collections.LibraryItemsThumbnails, admin: { width: "33%" }, }), ], @@ -243,6 +258,9 @@ export const LibraryItems = buildVersionedCollectionConfig( type: "array", admin: { 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: { RowLabel: ({ data }) => RowLabel(data), }, @@ -259,7 +277,7 @@ export const LibraryItems = buildVersionedCollectionConfig( }, imageField({ name: fields.scansPagesImage, - relationTo: LibraryItemThumbnails.slug, + relationTo: Collections.LibraryItemsThumbnails, required: true, admin: { width: "66%" }, }), @@ -314,7 +332,7 @@ export const LibraryItems = buildVersionedCollectionConfig( { name: fields.priceCurrency, type: "relationship", - relationTo: Currencies.slug, + relationTo: Collections.Currencies, required: true, admin: { allowCreate: false, width: "50%" }, }, @@ -344,7 +362,7 @@ export const LibraryItems = buildVersionedCollectionConfig( name: fields.textualSubtype, label: "Subtype", type: "relationship", - relationTo: [Keys.slug], + relationTo: [Collections.Keys], filterOptions: { type: { equals: KeysTypes.LibraryTextual } }, hasMany: true, admin: { allowCreate: false, width: "50%" }, @@ -352,7 +370,7 @@ export const LibraryItems = buildVersionedCollectionConfig( { name: fields.textualLanguages, type: "relationship", - relationTo: [Languages.slug], + relationTo: [Collections.Languages], hasMany: true, admin: { allowCreate: false, width: "50%" }, }, @@ -406,7 +424,7 @@ export const LibraryItems = buildVersionedCollectionConfig( name: fields.audioSubtype, label: "Subtype", type: "relationship", - relationTo: [Keys.slug], + relationTo: [Collections.Keys], filterOptions: { type: { equals: KeysTypes.LibraryAudio } }, hasMany: true, 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, type: "date", diff --git a/src/collections/LibraryItems/endpoints/getSlug.ts b/src/collections/LibraryItems/endpoints/getBySlug.ts similarity index 68% rename from src/collections/LibraryItems/endpoints/getSlug.ts rename to src/collections/LibraryItems/endpoints/getBySlug.ts index 0515e51..b540b33 100644 --- a/src/collections/LibraryItems/endpoints/getSlug.ts +++ b/src/collections/LibraryItems/endpoints/getBySlug.ts @@ -1,20 +1,22 @@ -import { LibraryItems } from "../LibraryItems"; -import { LibraryItem } from "../../../types/collections"; 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 & { size?: Omit; price?: Omit & { currency: string }; - scans?: Omit & { - obibelt: Omit; - cover: Omit; - dustjacket: Omit; + scans?: Omit & { + obi: Omit; + cover: Omit; + dustjacket: Omit; }; }; -export const getSlug = (collectionSlug: string) => - createBySlugEndpoint(collectionSlug, ({ id, size, price, scans, ...otherProps }) => { +export const getBySlug = createGetByEndpoint>( + Collections.LibraryItems, + "slug", + async ({ id, size, price, scans, ...otherProps }) => { const processedLibraryItem: ProcessedLibraryItem = { size: processOptionalGroup(size), price: processPrice(price), @@ -30,15 +32,16 @@ export const getSlug = (collectionSlug: string) => undefinedValues: true, NaNValues: false, }); - }); + } +); const processScans = (scans: LibraryItem["scans"]): ProcessedLibraryItem["scans"] => { if (!scans || scans.length === 0) return undefined; - const { cover, dustjacket, id, obibelt, ...otherProps } = scans[0]; + const { cover, dustjacket, id, obi, ...otherProps } = scans[0]; return { cover: processOptionalGroup(cover), dustjacket: processOptionalGroup(dustjacket), - obibelt: processOptionalGroup(obibelt), + obi: processOptionalGroup(obi), ...otherProps, }; }; diff --git a/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts b/src/collections/LibraryItemsThumbnails/LibraryItemsThumbnails.ts similarity index 87% rename from src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts rename to src/collections/LibraryItemsThumbnails/LibraryItemsThumbnails.ts index 0706faf..dba6ffc 100644 --- a/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts +++ b/src/collections/LibraryItemsThumbnails/LibraryItemsThumbnails.ts @@ -1,4 +1,4 @@ -import { CollectionGroup } from "../../constants"; +import { CollectionGroups, Collections } from "../../constants"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -7,7 +7,8 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const LibraryItemThumbnails = buildCollectionConfig( +export const LibraryItemsThumbnails = buildCollectionConfig( + Collections.LibraryItemsThumbnails, { singular: "Library Item Thumbnail", plural: "Library Item Thumbnails", @@ -17,7 +18,7 @@ export const LibraryItemThumbnails = buildCollectionConfig( admin: { useAsTitle: fields.filename, disableDuplicate: true, - group: CollectionGroup.Media, + group: CollectionGroups.Media, }, upload: { staticDir: uploadDir, diff --git a/src/collections/Posts/Posts.ts b/src/collections/Posts/Posts.ts index 26c9039..bb70d3f 100644 --- a/src/collections/Posts/Posts.ts +++ b/src/collections/Posts/Posts.ts @@ -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 { CollectionGroup, KeysTypes } from "../../constants"; -import { Recorders } from "../Recorders/Recorders"; +import { slugField } from "../../fields/slugField/slugField"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; -import { isDefined, isUndefined } from "../../utils/asserts"; -import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate"; -import { Keys } from "../Keys/Keys"; -import { PostThumbnails } from "../PostThumbnails/PostThumbnails"; -import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; +import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; 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 = { slug: "slug", @@ -29,6 +27,7 @@ const fields = { } as const satisfies Record; export const Posts = buildVersionedCollectionConfig( + Collections.Posts, { singular: "Post", plural: "Posts", @@ -41,7 +40,16 @@ export const Posts = buildVersionedCollectionConfig( "News articles written by our Recorders! Here you will find announcements about \ new merch/items releases, guides, theories, unboxings, showcases...", defaultColumns: [fields.slug, fields.thumbnail, fields.categories], - group: CollectionGroup.Collections, + group: CollectionGroups.Collections, + components: { + BeforeListTable: [ + () => + QuickFilters({ + slug: Collections.Posts, + filterGroups: [publishStatusFilters], + }), + ], + }, hooks: { beforeDuplicate: beforeDuplicatePiping([ beforeDuplicateUnpublish, @@ -60,7 +68,7 @@ export const Posts = buildVersionedCollectionConfig( slugField({ name: fields.slug, admin: { width: "50%" } }), imageField({ name: fields.thumbnail, - relationTo: PostThumbnails.slug, + relationTo: Collections.PostsThumbnails, admin: { width: "50%" }, }), ], @@ -71,7 +79,7 @@ export const Posts = buildVersionedCollectionConfig( { name: fields.authors, type: "relationship", - relationTo: [Recorders.slug], + relationTo: [Collections.Recorders], required: true, minRows: 1, hasMany: true, @@ -80,7 +88,7 @@ export const Posts = buildVersionedCollectionConfig( { name: fields.categories, type: "relationship", - relationTo: [Keys.slug], + relationTo: [Collections.Keys], filterOptions: { type: { equals: KeysTypes.Categories } }, hasMany: true, admin: { allowCreate: false, width: "35%" }, @@ -101,7 +109,7 @@ export const Posts = buildVersionedCollectionConfig( { name: fields.translators, type: "relationship", - relationTo: Recorders.slug, + relationTo: Collections.Recorders, hasMany: true, admin: { condition: (_, siblingData) => { @@ -134,7 +142,7 @@ export const Posts = buildVersionedCollectionConfig( { name: fields.proofreaders, type: "relationship", - relationTo: Recorders.slug, + relationTo: Collections.Recorders, hasMany: true, admin: { width: "50%" }, }, diff --git a/src/collections/PostThumbnails/PostThumbnails.ts b/src/collections/PostsThumbnails/PostsThumbnails.ts similarity index 84% rename from src/collections/PostThumbnails/PostThumbnails.ts rename to src/collections/PostsThumbnails/PostsThumbnails.ts index 25e1ba9..0ce2b1c 100644 --- a/src/collections/PostThumbnails/PostThumbnails.ts +++ b/src/collections/PostsThumbnails/PostsThumbnails.ts @@ -1,4 +1,4 @@ -import { CollectionGroup } from "../../constants"; +import { CollectionGroups, Collections } from "../../constants"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -7,7 +7,8 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const PostThumbnails = buildCollectionConfig( +export const PostsThumbnails = buildCollectionConfig( + Collections.PostsThumbnails, { singular: "Post Thumbnail", plural: "Post Thumbnails", @@ -17,7 +18,7 @@ export const PostThumbnails = buildCollectionConfig( admin: { useAsTitle: fields.filename, disableDuplicate: true, - group: CollectionGroup.Media, + group: CollectionGroups.Media, }, upload: { staticDir: uploadDir, diff --git a/src/collections/Recorders/Recorders.ts b/src/collections/Recorders/Recorders.ts index 66a52b8..88ca1d8 100644 --- a/src/collections/Recorders/Recorders.ts +++ b/src/collections/Recorders/Recorders.ts @@ -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 { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole"; +import { mustBeAdmin } from "../../accesses/mustBeAdmin"; 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 = { username: "username", @@ -20,6 +19,7 @@ const fields = { } as const satisfies Record; export const Recorders = buildCollectionConfig( + Collections.Recorders, { singular: "Recorder", plural: "Recorders", @@ -40,17 +40,25 @@ export const Recorders = buildCollectionConfig( fields.role, ], disableDuplicate: true, - group: CollectionGroup.Meta, + group: CollectionGroups.Meta, components: { BeforeListTable: [ () => QuickFilters({ - route: "/admin/collections/recorders", - filters: [ - { label: "Admins", filter: "where[role][equals]=Admin" }, - { label: "Recorders", filter: "where[role][equals]=Recorder" }, - { label: "∅ Role", filter: "where[role][not_in]=Admin,Recorder" }, - { label: "Anonymized", filter: "where[anonymize][equals]=true" }, + slug: Collections.Recorders, + filterGroups: [ + [ + ...Object.entries(RecordersRoles).map(([key, value]) => ({ + label: value, + 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: { beforeLogin: [beforeLoginMustHaveAtLeastOneRole], }, + endpoints: [importFromStrapi], timestamps: false, fields: [ { @@ -80,7 +89,7 @@ export const Recorders = buildCollectionConfig( }, imageField({ name: fields.avatar, - relationTo: RecorderThumbnails.slug, + relationTo: Collections.RecordersThumbnails, admin: { width: "66%" }, }), ], @@ -88,7 +97,7 @@ export const Recorders = buildCollectionConfig( { name: fields.languages, type: "relationship", - relationTo: Languages.slug, + relationTo: Collections.Languages, hasMany: true, admin: { allowCreate: false, diff --git a/src/collections/Recorders/endpoints/importFromStrapi.ts b/src/collections/Recorders/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..96c7fcb --- /dev/null +++ b/src/collections/Recorders/endpoints/importFromStrapi.ts @@ -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({ + 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 = { + 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 }); + }, + }, +}); diff --git a/src/collections/RecordersThumbnails/RecordersThumbnails.ts b/src/collections/RecordersThumbnails/RecordersThumbnails.ts new file mode 100644 index 0000000..49221d0 --- /dev/null +++ b/src/collections/RecordersThumbnails/RecordersThumbnails.ts @@ -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; + +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 } }), + }), + ], + }) +); diff --git a/src/collections/Videos/Videos.ts b/src/collections/Videos/Videos.ts new file mode 100644 index 0000000..f19f0d5 --- /dev/null +++ b/src/collections/Videos/Videos.ts @@ -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; + +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" }, + }, + ], + }) +); diff --git a/src/collections/Videos/endpoints/importFromStrapi.ts b/src/collections/Videos/endpoints/importFromStrapi.ts new file mode 100644 index 0000000..13b073f --- /dev/null +++ b/src/collections/Videos/endpoints/importFromStrapi.ts @@ -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