Compare commits
1 Commits
main
...
dependabot
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | 794a228fee |
44
.env.example
44
.env.example
|
@ -1,44 +0,0 @@
|
|||
# /!\ For URLs, don't include the trailing '/'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
# │ PRIVATE VARIABLES │
|
||||
# └─────────────────────┘
|
||||
|
||||
## STRAPI
|
||||
|
||||
URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql
|
||||
ACCESS_TOKEN=abcdef0123456789
|
||||
REVALIDATION_TOKEN=abcdef0123456789
|
||||
|
||||
## MAILING
|
||||
|
||||
SMTP_HOST=email.provider.com
|
||||
SMTP_USER=email@example.com
|
||||
SMTP_PASSWORD=mypassword123
|
||||
|
||||
|
||||
|
||||
# ┌────────────────────┐
|
||||
# │ PUBLIC VARIABLES │
|
||||
# └────────────────────┘
|
||||
|
||||
## ASSETS
|
||||
|
||||
NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com
|
||||
NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com
|
||||
NEXT_PUBLIC_URL_SELF=https://url-to.front-accords-library.com
|
||||
NEXT_PUBLIC_URL_ASSETS=https://url-to.assets-accords-library.com
|
||||
|
||||
## MEILISEARCH
|
||||
|
||||
NEXT_PUBLIC_URL_MEILISEARCH=https://url-to.search-accords-library.com
|
||||
NEXT_PUBLIC_MEILISEARCH_KEY=abcdef0123456789
|
||||
|
||||
## UMAMI
|
||||
|
||||
NEXT_PUBLIC_UMAMI_URL=https://url-to.umami-accords-library.com
|
||||
NEXT_PUBLIC_UMAMI_ID=abcdef0123456789
|
||||
|
||||
## OCR.SPACE
|
||||
|
||||
NEXT_PUBLIC_API_OCR_KEY=abcdef0123456789
|
|
@ -161,6 +161,7 @@ module.exports = {
|
|||
"@typescript-eslint/no-invalid-void-type": "error",
|
||||
"@typescript-eslint/no-meaningless-void-operator": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error",
|
||||
"@typescript-eslint/no-parameter-properties": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
// "@typescript-eslint/no-type-alias": "warn",
|
||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
|
||||
|
@ -181,6 +182,7 @@ module.exports = {
|
|||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/sort-type-union-intersection-members": "warn",
|
||||
// "@typescript-eslint/strict-boolean-expressions": [
|
||||
// "error",
|
||||
// { allowAny: true },
|
||||
|
@ -190,6 +192,7 @@ module.exports = {
|
|||
"@typescript-eslint/unified-signatures": "error",
|
||||
|
||||
/* EXTENSION OF ESLINT */
|
||||
"@typescript-eslint/no-duplicate-imports": "error",
|
||||
"@typescript-eslint/default-param-last": "warn",
|
||||
"@typescript-eslint/dot-notation": "warn",
|
||||
"@typescript-eslint/init-declarations": "warn",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# Generated content
|
||||
src/graphql/generated.ts
|
||||
|
||||
public/robots.txt
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
|
|
@ -3,3 +3,4 @@ interactive: true
|
|||
format: "group"
|
||||
reject:
|
||||
- "react-hotkeys-hook" # we are stuck at version 3.4.7 because 4.X is not working well. Need more experimenting.
|
||||
- "graphql-request" # we are stuck at version 5.1.0 because 5.2.0 has a typescript bug see https://github.com/dotansimha/graphql-code-generator/issues/9046
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
.next
|
||||
public/local-data/*
|
38
README.md
38
README.md
|
@ -67,7 +67,7 @@ A detailled look at the technologies used in this repository:
|
|||
- The website is built before running in production
|
||||
- Performances are great, and it's possible to deploy the app on a CDN
|
||||
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
|
||||
- Some widely used data (e.g: UI localizations) are downloaded separetely into `public/local-data` as some form of request deduping + it make this data hot-swappable without the need to rebuild the entire website.
|
||||
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every pages.
|
||||
|
||||
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
|
||||
|
||||
|
@ -102,15 +102,17 @@ A detailled look at the technologies used in this repository:
|
|||
|
||||
- Multilingual
|
||||
|
||||
- Users are given a list of supported languages. The first language in this list is the primary language (the language of the UI), the others are fallback languages. The others are fallback languages.
|
||||
- By default, the list is ordered following the browser's languages (and most spoken languages woldwide for the remaining languages). The list can also be reordered manually.
|
||||
- Contents can be available in any number of languages. By default, the best matching language will be presented to the user. However, the user can also decide to temporary select another language for a specific content, without affecting their list of preferred languages.
|
||||
- By default, use the browser's language as the main language
|
||||
- Fallback languages are used for content which are not available in the main language
|
||||
- Main and fallback languages can be ordered manually by the user
|
||||
- At the content level, the user can know which language is available
|
||||
- Furthermore, the user can temporary select another language then the one that was automatically selected
|
||||
|
||||
- UI Localizations
|
||||
|
||||
- The translated wordings use [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to include variables, plural, dates...
|
||||
- Use a custom ICU Typescript transformation script to provide type safety when formatting ICU wordings
|
||||
- Fallback to English if the translation is missing.
|
||||
- Fallback to English if a specific working isn't available in the user's language
|
||||
|
||||
- SEO
|
||||
|
||||
|
@ -132,11 +134,10 @@ A detailled look at the technologies used in this repository:
|
|||
- [ts-unused-exports](https://www.npmjs.com/package/ts-unused-exports) to find unused exported functions/constants...
|
||||
|
||||
- Other
|
||||
|
||||
- Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader)
|
||||
- Support for [Material Symbols](https://fonts.google.com/icons)
|
||||
- Custom lightbox using [react-zoom-pan-pinch](https://www.npmjs.com/package/react-zoom-pan-pinch)
|
||||
- Handle query params type-validation using [Zod](https://zod.dev/)
|
||||
- Handle query params using [Zod](https://zod.dev/)
|
||||
- A secret "Terminal" mode. Can you find it?
|
||||
|
||||
## Installation
|
||||
|
@ -147,14 +148,31 @@ cd accords-library.com
|
|||
npm install
|
||||
```
|
||||
|
||||
Create a env file based on the example one:
|
||||
Create a env file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
nano .env.local
|
||||
```
|
||||
|
||||
Change the variables
|
||||
Enter the following information:
|
||||
|
||||
```
|
||||
URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql
|
||||
ACCESS_TOKEN=abcdef0123456789
|
||||
REVALIDATION_TOKEN=abcdef0123456789
|
||||
SMTP_HOST=email.provider.com
|
||||
SMTP_USER=email@example.com
|
||||
SMTP_PASSWORD=mypassword123
|
||||
NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com
|
||||
NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com
|
||||
NEXT_PUBLIC_URL_WATCH=https://url-to.watch-accords-library.com
|
||||
NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com
|
||||
NEXT_PUBLIC_URL_MEILISEARCH=https://url-to.search-accords-library.com
|
||||
NEXT_PUBLIC_MEILISEARCH_KEY=abcdef0123456789
|
||||
NEXT_PUBLIC_UMAMI_URL=https://url-to.umami-accords-library.com
|
||||
NEXT_PUBLIC_UMAMI_ID=abcdef0123456789
|
||||
NEXT_PUBLIC_API_OCR_KEY=abcdef0123456789
|
||||
```
|
||||
|
||||
Run in dev mode:
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* CONFIG */
|
||||
|
||||
const locales = ["en", "es", "fr", "pt-br", "ja", "zh"];
|
||||
const locales = ["en", "es", "fr", "pt-br", "ja"];
|
||||
|
||||
/* END CONFIG */
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
104
package.json
104
package.json
|
@ -4,8 +4,7 @@
|
|||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"dev": "next dev -p 12499",
|
||||
"precommit": "npm run fetch-local-data && npm run icu-to-ts && npm run unused-wording-keys && npm run unused-exports && npm run prettier && npm run eslint && npm run tsc && echo ALL PRECOMMIT CHECKS PASSED SUCCESSFULLY, LET\\'S FUCKING GO!",
|
||||
"unused-wording-keys": "esrun --send-code-mode=temporaryFile src/graphql/unusedWordingKeys.ts --uwk",
|
||||
"precommit": "npm run fetch-local-data && npm run icu-to-ts && npm run unused-exports && npm run prettier && npm run eslint && npm run tsc && echo ALL PRECOMMIT CHECKS PASSED SUCCESSFULLY, LET\\'S FUCKING GO!",
|
||||
"unused-exports": "ts-unused-exports ./tsconfig.json --excludePathsFromReport='src/pages;tailwind.config.ts;src/graphql/generated.ts;src/shared/meilisearch-graphql-typings/generated.ts'",
|
||||
"fetch-local-data": "npm run generate && esrun --send-code-mode=temporaryFile src/graphql/fetchLocalData.ts --esrun",
|
||||
"icu-to-ts": "esrun --send-code-mode=temporaryFile src/graphql/icuToTypescript.ts --icu",
|
||||
|
@ -17,75 +16,78 @@
|
|||
"eslint": "npx eslint .",
|
||||
"generate": "graphql-codegen --config graphql-codegen.config.js",
|
||||
"tsc": "tsc",
|
||||
"prettier": "prettier --list-different --end-of-line auto --write .",
|
||||
"prettier": "prettier --end-of-line auto --write .",
|
||||
"upgrade": "ncu"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-serif-jp": "^5.0.7",
|
||||
"@fontsource/opendyslexic": "^5.0.7",
|
||||
"@fontsource/share-tech-mono": "^5.0.8",
|
||||
"@fontsource/vollkorn": "^5.0.9",
|
||||
"@fontsource/zen-maru-gothic": "^5.0.7",
|
||||
"@formatjs/icu-messageformat-parser": "^2.6.0",
|
||||
"@fontsource/noto-serif-jp": "^4.5.12",
|
||||
"@fontsource/opendyslexic": "^4.5.4",
|
||||
"@fontsource/share-tech-mono": "^4.5.9",
|
||||
"@fontsource/vollkorn": "^4.5.14",
|
||||
"@fontsource/zen-maru-gothic": "^4.5.16",
|
||||
"@formatjs/icu-messageformat-parser": "^2.3.1",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cuid": "^2.1.8",
|
||||
"html-to-text": "^9.0.5",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"isomorphic-dompurify": "^1.8.0",
|
||||
"jotai": "^2.3.1",
|
||||
"markdown-to-jsx": "^7.3.2",
|
||||
"marked": "^7.0.3",
|
||||
"material-symbols": "^0.10.4",
|
||||
"meilisearch": "^0.34.1",
|
||||
"next": "^13.4.17",
|
||||
"nodemailer": "^6.9.4",
|
||||
"patch-package": "^8.0.0",
|
||||
"rc-slider": "^10.2.1",
|
||||
"intl-messageformat": "^10.3.4",
|
||||
"isomorphic-dompurify": "^1.3.0",
|
||||
"jotai": "^2.0.4",
|
||||
"markdown-to-jsx": "^7.2.0",
|
||||
"marked": "^4.3.0",
|
||||
"material-symbols": "^0.5.5",
|
||||
"meilisearch": "^0.32.3",
|
||||
"next": "^13.3.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"patch-package": "^7.0.0",
|
||||
"rc-slider": "^10.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-collapsible": "^2.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-zoom-pan-pinch": "^3.1.0",
|
||||
"react-hotkeys-hook": "^4.4.0",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"react-zoom-pan-pinch": "^3.0.7",
|
||||
"string-natural-compare": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"turndown": "^7.1.2",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"zod": "^3.22.1"
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "3.2.24",
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-graphql-request": "5.0.0",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@types/html-to-text": "^9.0.1",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@types/node": "20.5.0",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/typescript": "3.0.4",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.9",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "18.16.3",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/string-natural-compare": "^3.0.2",
|
||||
"@types/throttle-debounce": "^5.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||
"@typescript-eslint/parser": "^6.4.0",
|
||||
"chalk": "^5.3.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-config-next": "13.4.17",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"graphql": "16.8.0",
|
||||
"graphql-request": "6.1.0",
|
||||
"next-sitemap": "^4.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"ts-unused-exports": "^10.0.0",
|
||||
"typescript": "^5.1.6"
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-next": "13.3.2",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "5.1.0",
|
||||
"next-sitemap": "^4.0.7",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"ts-unused-exports": "^9.0.4",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"overrides": {
|
||||
"react-zoom-pan-pinch": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -1 +1,91 @@
|
|||
{"currencies":{"data":[{"id":"1","attributes":{"code":"EUR","symbol":"€","rate_to_usd":1.036166,"display_decimals":true}},{"id":"2","attributes":{"code":"CAD","symbol":"$","rate_to_usd":0.79319156,"display_decimals":true}},{"id":"3","attributes":{"code":"USD","symbol":"$","rate_to_usd":1,"display_decimals":true}},{"id":"4","attributes":{"code":"JPY","symbol":"¥","rate_to_usd":0.0083864261,"display_decimals":false}},{"id":"5","attributes":{"code":"BRL","symbol":"R$","rate_to_usd":0.19904328,"display_decimals":true}},{"id":"6","attributes":{"code":"GBP","symbol":"£","rate_to_usd":1.3181323,"display_decimals":true}},{"id":"7","attributes":{"code":"AUD","symbol":"$","rate_to_usd":0.7422,"display_decimals":true}},{"id":"8","attributes":{"code":"INR","symbol":"₹","rate_to_usd":0.013162881,"display_decimals":false}},{"id":"9","attributes":{"code":"NZD","symbol":"$","rate_to_usd":0.69089984,"display_decimals":true}},{"id":"10","attributes":{"code":"CHF","symbol":"CHF","rate_to_usd":1.0728706,"display_decimals":true}},{"id":"11","attributes":{"code":"CNY","symbol":"¥","rate_to_usd":0.141546,"display_decimals":true}}]}}
|
||||
{
|
||||
"currencies": {
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"attributes": {
|
||||
"code": "EUR",
|
||||
"symbol": "€",
|
||||
"rate_to_usd": 1.036166,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"attributes": {
|
||||
"code": "CAD",
|
||||
"symbol": "$",
|
||||
"rate_to_usd": 0.79319156,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"attributes": { "code": "USD", "symbol": "$", "rate_to_usd": 1, "display_decimals": true }
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"attributes": {
|
||||
"code": "JPY",
|
||||
"symbol": "¥",
|
||||
"rate_to_usd": 0.0083864261,
|
||||
"display_decimals": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"attributes": {
|
||||
"code": "BRL",
|
||||
"symbol": "R$",
|
||||
"rate_to_usd": 0.19904328,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"attributes": {
|
||||
"code": "GBP",
|
||||
"symbol": "£",
|
||||
"rate_to_usd": 1.3181323,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"attributes": {
|
||||
"code": "AUD",
|
||||
"symbol": "$",
|
||||
"rate_to_usd": 0.7422,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"attributes": {
|
||||
"code": "INR",
|
||||
"symbol": "₹",
|
||||
"rate_to_usd": 0.013162881,
|
||||
"display_decimals": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"attributes": {
|
||||
"code": "NZD",
|
||||
"symbol": "$",
|
||||
"rate_to_usd": 0.69089984,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"attributes": {
|
||||
"code": "CHF",
|
||||
"symbol": "CHF",
|
||||
"rate_to_usd": 1.0728706,
|
||||
"display_decimals": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,36 @@
|
|||
{"languages":{"data":[{"id":"1","attributes":{"name":"French","code":"fr","localized_name":"Français"}},{"id":"2","attributes":{"name":"English","code":"en","localized_name":"English"}},{"id":"3","attributes":{"name":"Japanese","code":"ja","localized_name":"日本語"}},{"id":"4","attributes":{"name":"Spanish","code":"es","localized_name":"Español"}},{"id":"6","attributes":{"name":"Portuguese (Brazil)","code":"pt-br","localized_name":"Português (Brasil)"}},{"id":"8","attributes":{"name":"German","code":"de","localized_name":"Deutsch"}},{"id":"9","attributes":{"name":"Italian","code":"it","localized_name":"Italiano"}},{"id":"10","attributes":{"name":"Russian","code":"ru","localized_name":"русский"}},{"id":"11","attributes":{"name":"Korean","code":"ko","localized_name":"한국어"}},{"id":"12","attributes":{"name":"Chinese","code":"zh","localized_name":"中文"}}]}}
|
||||
{
|
||||
"languages": {
|
||||
"data": [
|
||||
{ "id": "1", "attributes": { "name": "French", "code": "fr", "localized_name": "Français" } },
|
||||
{ "id": "2", "attributes": { "name": "English", "code": "en", "localized_name": "English" } },
|
||||
{ "id": "3", "attributes": { "name": "Japanese", "code": "ja", "localized_name": "日本語" } },
|
||||
{ "id": "4", "attributes": { "name": "Spanish", "code": "es", "localized_name": "Español" } },
|
||||
{
|
||||
"id": "6",
|
||||
"attributes": {
|
||||
"name": "Portuguese (Brazil)",
|
||||
"code": "pt-br",
|
||||
"localized_name": "Português (Brasil)"
|
||||
}
|
||||
},
|
||||
{ "id": "8", "attributes": { "name": "German", "code": "de", "localized_name": "Deutsch" } },
|
||||
{
|
||||
"id": "9",
|
||||
"attributes": { "name": "Italian", "code": "it", "localized_name": "Italiano" }
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"attributes": { "name": "Russian", "code": "ru", "localized_name": "русский" }
|
||||
},
|
||||
{ "id": "11", "attributes": { "name": "Korean", "code": "ko", "localized_name": "한국어" } },
|
||||
{
|
||||
"id": "12",
|
||||
"attributes": {
|
||||
"name": "Chinese (Traditional)",
|
||||
"code": "zh-cht",
|
||||
"localized_name": "中文(繁體)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,6 @@
|
|||
import Head from "next/head";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { atom } from "jotai";
|
||||
import { useRouter } from "next/router";
|
||||
import { layout } from "../../design.config";
|
||||
import { Ico } from "./Ico";
|
||||
import { MainPanel } from "./Panels/MainPanel";
|
||||
|
@ -20,7 +18,6 @@ import { useFormat } from "hooks/useFormat";
|
|||
*/
|
||||
|
||||
const SENSIBILITY_SWIPE = 1.1;
|
||||
const isIOSAtom = atom((get) => get(atoms.userAgent.os) === "iOS");
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -51,14 +48,12 @@ export const AppLayout = ({
|
|||
const [isSubPanelOpened, setSubPanelOpened] = useAtomPair(atoms.layout.subPanelOpened);
|
||||
const [isMainPanelOpened, setMainPanelOpened] = useAtomPair(atoms.layout.mainPanelOpened);
|
||||
const isMenuGesturesEnabled = useAtomGetter(atoms.layout.menuGesturesEnabled);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
|
||||
const isIOS = useAtomGetter(isIOSAtom);
|
||||
const router = useRouter();
|
||||
|
||||
const { format } = useFormat();
|
||||
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
|
||||
|
||||
const handlers = useSwipeable({
|
||||
onSwipedLeft: (SwipeEventData) => {
|
||||
if (isMenuGesturesEnabled) {
|
||||
|
@ -107,49 +102,26 @@ export const AppLayout = ({
|
|||
<title>{openGraph.title}</title>
|
||||
<meta name="description" content={openGraph.description} />
|
||||
|
||||
<meta name="twitter:site" content="@AccordsLibrary" />
|
||||
<meta name="twitter:title" content={openGraph.title} />
|
||||
<meta name="twitter:description" content={openGraph.description} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={openGraph.thumbnail.image} />
|
||||
|
||||
<meta
|
||||
property="og:type"
|
||||
content={openGraph.video ? "video.movie" : openGraph.audio ? "music.song" : "website"}
|
||||
/>
|
||||
<meta property="og:locale" content={router.locale} />
|
||||
<meta property="og:site_name" content="Accord’s Library" />
|
||||
|
||||
<meta property="og:title" content={openGraph.title} />
|
||||
<meta property="og:description" content={openGraph.description} />
|
||||
|
||||
<meta property="og:image" content={openGraph.thumbnail.image} />
|
||||
<meta property="og:image:secure_url" content={openGraph.thumbnail.image} />
|
||||
<meta property="og:image:width" content={openGraph.thumbnail.width.toString()} />
|
||||
<meta property="og:image:height" content={openGraph.thumbnail.height.toString()} />
|
||||
<meta property="og:image:alt" content={openGraph.thumbnail.alt} />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
|
||||
{openGraph.audio && (
|
||||
<>
|
||||
<meta property="og:audio" content={openGraph.audio} />
|
||||
<meta property="og:audio:type" content="audio/mpeg" />
|
||||
</>
|
||||
)}
|
||||
{openGraph.video && (
|
||||
<>
|
||||
<meta property="og:video" content={openGraph.video} />{" "}
|
||||
<meta property="og:video:type" content="video/mp4" />
|
||||
</>
|
||||
)}
|
||||
</Head>
|
||||
|
||||
{/* Content panel */}
|
||||
<div
|
||||
id={Ids.ContentPanel}
|
||||
className={cJoin(
|
||||
"bg-light [grid-area:content]",
|
||||
cIf(!isIOS, "texture-paper-dots"),
|
||||
"bg-light texture-paper-dots [grid-area:content]",
|
||||
cIf(contentPanelScroolbar, "overflow-y-scroll")
|
||||
)}>
|
||||
{isDefined(contentPanel) ? (
|
||||
|
@ -162,14 +134,13 @@ export const AppLayout = ({
|
|||
</div>
|
||||
|
||||
{/* Background when navbar is opened */}
|
||||
|
||||
<div
|
||||
className={cJoin(
|
||||
`absolute inset-0 z-40 transition-filter duration-500
|
||||
`absolute inset-0 transition-filter duration-500
|
||||
[grid-area:content]`,
|
||||
cIf(
|
||||
(isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout,
|
||||
cIf(!isPerfModeEnabled, "backdrop-blur"),
|
||||
"backdrop-blur",
|
||||
"pointer-events-none touch-none"
|
||||
)
|
||||
)}>
|
||||
|
@ -193,8 +164,7 @@ export const AppLayout = ({
|
|||
<div
|
||||
className={cJoin(
|
||||
`z-40 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
|
||||
border-dotted border-black bg-light [grid-area:navbar]`,
|
||||
cIf(!isIOS, "texture-paper-dots"),
|
||||
border-dotted border-black bg-light texture-paper-dots [grid-area:navbar]`,
|
||||
cIf(!is1ColumnLayout, "hidden")
|
||||
)}>
|
||||
<Ico
|
||||
|
@ -231,12 +201,11 @@ export const AppLayout = ({
|
|||
<div
|
||||
id={Ids.SubPanel}
|
||||
className={cJoin(
|
||||
`overflow-y-scroll border-r border-dark/50 bg-light
|
||||
transition-transform duration-300 scrollbar-none`,
|
||||
cIf(!isIOS, "texture-paper-dots"),
|
||||
`z-40 overflow-y-scroll border-r border-dark/50 bg-light
|
||||
transition-transform duration-300 scrollbar-none texture-paper-dots`,
|
||||
cIf(
|
||||
is1ColumnLayout,
|
||||
"z-40 justify-self-end border-r-0 [grid-area:content]",
|
||||
"justify-self-end border-r-0 [grid-area:content]",
|
||||
"[grid-area:sub]"
|
||||
),
|
||||
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l"),
|
||||
|
@ -249,10 +218,9 @@ export const AppLayout = ({
|
|||
{/* Main panel */}
|
||||
<div
|
||||
className={cJoin(
|
||||
`overflow-y-scroll border-r border-dark/50 bg-light
|
||||
transition-transform duration-300 scrollbar-none`,
|
||||
cIf(!isIOS, "texture-paper-dots"),
|
||||
cIf(is1ColumnLayout, "z-40 justify-self-start [grid-area:content]", "[grid-area:main]"),
|
||||
`z-40 overflow-y-scroll border-r border-dark/50 bg-light
|
||||
transition-transform duration-300 scrollbar-none texture-paper-dots`,
|
||||
cIf(is1ColumnLayout, "justify-self-start [grid-area:content]", "[grid-area:main]"),
|
||||
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)]"),
|
||||
cIf(!isMainPanelOpened && is1ColumnLayout, "-translate-x-full")
|
||||
)}>
|
||||
|
|
|
@ -16,7 +16,8 @@ export const Chip = ({ className, text }: Props): JSX.Element => (
|
|||
<div
|
||||
className={cJoin(
|
||||
`grid place-content-center place-items-center whitespace-nowrap rounded-full border
|
||||
border-black/70 px-1.5 pb-[0.14rem] text-xs text-black/70`,
|
||||
px-1.5 pb-[0.14rem] text-xs opacity-70 transition-[color,opacity,border-color]
|
||||
hover:opacity-100`,
|
||||
className
|
||||
)}>
|
||||
{text}
|
||||
|
|
|
@ -19,11 +19,10 @@ interface Props {
|
|||
export const ChroniclesLists = ({ chapters, currentChronicleSlug }: Props): JSX.Element => {
|
||||
const [openedIndex, setOpenedIndex] = useState(
|
||||
currentChronicleSlug
|
||||
? chapters.findIndex(
|
||||
(chapter) =>
|
||||
chapter.attributes?.chronicles?.data.some(
|
||||
(chronicle) => chronicle.attributes?.slug === currentChronicleSlug
|
||||
)
|
||||
? chapters.findIndex((chapter) =>
|
||||
chapter.attributes?.chronicles?.data.some(
|
||||
(chronicle) => chronicle.attributes?.slug === currentChronicleSlug
|
||||
)
|
||||
)
|
||||
: -1
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { cJoin } from "helpers/className";
|
|||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { atoms } from "contexts/atoms";
|
|||
import { isUndefined } from "helpers/asserts";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollOnChange";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { Ids } from "types/ids";
|
||||
|
||||
/*
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
|
||||
/*
|
||||
|
@ -11,11 +11,11 @@ import { Button } from "components/Inputs/Button";
|
|||
*/
|
||||
|
||||
interface Props {
|
||||
onOpen?: () => void;
|
||||
onCloseRequest?: () => void;
|
||||
isVisible: boolean;
|
||||
children: React.ReactNode;
|
||||
fillViewport?: boolean;
|
||||
hideBackground?: boolean;
|
||||
padding?: boolean;
|
||||
withCloseButton?: boolean;
|
||||
}
|
||||
|
@ -23,18 +23,17 @@ interface Props {
|
|||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Popup = ({
|
||||
onOpen,
|
||||
onCloseRequest,
|
||||
isVisible,
|
||||
children,
|
||||
fillViewport,
|
||||
hideBackground = false,
|
||||
padding = true,
|
||||
withCloseButton = true,
|
||||
}: Props): JSX.Element => {
|
||||
const setMenuGesturesEnabled = useAtomSetter(atoms.layout.menuGesturesEnabled);
|
||||
const [isHidden, setHidden] = useState(!isVisible);
|
||||
const [isActuallyVisible, setActuallyVisible] = useState(isVisible && !isHidden);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
|
||||
useHotkeys("escape", () => onCloseRequest?.(), { enabled: isVisible }, [onCloseRequest]);
|
||||
|
||||
|
@ -48,18 +47,13 @@ export const Popup = ({
|
|||
if (isVisible) {
|
||||
setHidden(false);
|
||||
// We delay the visiblity of the element so that the opening animation is played
|
||||
timeouts.push(
|
||||
setTimeout(() => {
|
||||
setActuallyVisible(true);
|
||||
onOpen?.();
|
||||
}, 100)
|
||||
);
|
||||
timeouts.push(setTimeout(() => setActuallyVisible(true), 100));
|
||||
} else {
|
||||
setActuallyVisible(false);
|
||||
timeouts.push(setTimeout(() => setHidden(true), 600));
|
||||
}
|
||||
return () => timeouts.forEach(clearTimeout);
|
||||
}, [isVisible, onOpen]);
|
||||
}, [isVisible]);
|
||||
|
||||
return isHidden ? (
|
||||
<></>
|
||||
|
@ -67,8 +61,7 @@ export const Popup = ({
|
|||
<div
|
||||
className={cJoin(
|
||||
"fixed inset-0 z-50 grid place-content-center transition-filter duration-500",
|
||||
cIf(!isActuallyVisible, "pointer-events-none touch-none"),
|
||||
cIf(isActuallyVisible && !isPerfModeEnabled, "backdrop-blur")
|
||||
cIf(isActuallyVisible, "backdrop-blur", "pointer-events-none touch-none")
|
||||
)}>
|
||||
<div
|
||||
className={cJoin(
|
||||
|
@ -80,15 +73,15 @@ export const Popup = ({
|
|||
|
||||
<div
|
||||
className={cJoin(
|
||||
`grid place-items-center gap-4 rounded-lg bg-light shadow-2xl transition-transform
|
||||
shadow-shade`,
|
||||
"grid place-items-center gap-4 transition-transform",
|
||||
cIf(padding, "p-10"),
|
||||
cIf(isActuallyVisible, "scale-100", "scale-0"),
|
||||
cIf(
|
||||
fillViewport,
|
||||
"absolute inset-10 content-start overflow-scroll",
|
||||
"relative max-h-[80vh] overflow-y-auto"
|
||||
)
|
||||
),
|
||||
cIf(!hideBackground, "rounded-lg bg-light shadow-2xl shadow-shade")
|
||||
)}>
|
||||
{withCloseButton && (
|
||||
<div className="absolute right-6 top-6">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { MouseEventHandler, useState } from "react";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -29,24 +27,20 @@ export const UpPressable = ({
|
|||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
onFocusChanged={setFocused}
|
||||
onClick={onClick}
|
||||
className={cJoin(
|
||||
"transition-all duration-300 !shadow-shade",
|
||||
cIf(isPerfModeEnabled, "shadow-lg", "drop-shadow-lg"),
|
||||
`drop-shadow-lg transition-all duration-300 shadow-shade`,
|
||||
cIf(!noBackground, "overflow-hidden rounded-md bg-highlight"),
|
||||
cIf(
|
||||
disabled,
|
||||
"cursor-not-allowed opacity-50 grayscale",
|
||||
cJoin(
|
||||
"cursor-pointer hover:scale-102",
|
||||
cIf(isPerfModeEnabled, "hover:shadow-xl", "hover:drop-shadow-xl"),
|
||||
cIf(isFocused, "hover:scale-105 hover:duration-100")
|
||||
"cursor-pointer hover:scale-102 hover:drop-shadow-xl",
|
||||
cIf(isFocused, "hover:scale-105 hover:drop-shadow-2xl hover:duration-100")
|
||||
)
|
||||
),
|
||||
className
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import { useRef } from "react";
|
||||
import { Button, TranslatedButton } from "components/Inputs/Button";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { ParentFolderPreviewFragment } from "graphql/generated";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { useScrollRightOnChange } from "hooks/useScrollOnChange";
|
||||
import { Ids } from "types/ids";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { Ico } from "components/Ico";
|
||||
|
||||
interface Props {
|
||||
path: ParentFolderPreviewFragment[];
|
||||
}
|
||||
|
||||
export const FolderPath = ({ path }: Props): JSX.Element => {
|
||||
useScrollRightOnChange(Ids.ContentsFolderPath, [path]);
|
||||
const setMenuGesturesEnabled = useAtomSetter(atoms.layout.menuGesturesEnabled);
|
||||
const gestureReenableTimeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div
|
||||
id={Ids.ContentsFolderPath}
|
||||
onPointerEnter={() => {
|
||||
if (gestureReenableTimeout.current) clearTimeout(gestureReenableTimeout.current);
|
||||
setMenuGesturesEnabled(false);
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
gestureReenableTimeout.current = setTimeout(() => setMenuGesturesEnabled(true), 500);
|
||||
}}
|
||||
className={`-mx-4 flex place-items-center justify-start gap-x-1 gap-y-4
|
||||
overflow-x-auto px-4 pb-10 scrollbar-none`}>
|
||||
{path.map((pathFolder, index) => (
|
||||
<>
|
||||
{pathFolder.slug === "root" ? (
|
||||
<Button href="/contents" icon="home" active={index === path.length - 1} />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
className="w-max"
|
||||
href={`/contents/folder/${pathFolder.slug}`}
|
||||
translations={filterHasAttributes(pathFolder.titles, [
|
||||
"language.data.attributes.code",
|
||||
]).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(pathFolder.slug),
|
||||
}}
|
||||
active={index === path.length - 1}
|
||||
/>
|
||||
)}
|
||||
{index < path.length - 1 && <Ico icon="chevron_right" />}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,126 +0,0 @@
|
|||
import { Chip } from "components/Chip";
|
||||
import { Markdawn } from "components/Markdown/Markdawn";
|
||||
import { RecorderChip } from "components/RecorderChip";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { ContentStatus, useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
languageCode?: string;
|
||||
sourceLanguageCode?: string;
|
||||
status?: ContentStatus | null;
|
||||
transcribers?: RecorderChipsProps["recorders"];
|
||||
translators?: RecorderChipsProps["recorders"];
|
||||
proofreaders?: RecorderChipsProps["recorders"];
|
||||
dubbers?: RecorderChipsProps["recorders"];
|
||||
subbers?: RecorderChipsProps["recorders"];
|
||||
authors?: RecorderChipsProps["recorders"];
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Credits = ({
|
||||
languageCode,
|
||||
sourceLanguageCode,
|
||||
status,
|
||||
transcribers = [],
|
||||
translators = [],
|
||||
dubbers = [],
|
||||
proofreaders = [],
|
||||
subbers = [],
|
||||
authors = [],
|
||||
notes,
|
||||
}: Props): JSX.Element => {
|
||||
const { format, formatStatusDescription, formatStatusLabel, formatLanguage } = useFormat();
|
||||
|
||||
return (
|
||||
<div className="grid place-items-center gap-5">
|
||||
{isDefined(languageCode) && isDefined(sourceLanguageCode) && (
|
||||
<>
|
||||
{languageCode === sourceLanguageCode ? (
|
||||
<h2 className="text-xl">{format("transcript_notice")}</h2>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-xl">{format("translation_notice")}</h2>
|
||||
<div className="flex flex-wrap place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{format("source_language")}:</p>
|
||||
<Chip text={formatLanguage(sourceLanguageCode)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{status && (
|
||||
<div className="flex flex-wrap place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{format("status")}:</p>
|
||||
<ToolTip content={formatStatusDescription(status)} maxWidth={"20rem"}>
|
||||
<Chip text={formatStatusLabel(status)} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transcribers.length > 0 && (
|
||||
<RecorderChips
|
||||
title={format("transcriber", { count: transcribers.length })}
|
||||
recorders={transcribers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{translators.length > 0 && (
|
||||
<RecorderChips
|
||||
title={format("translator", { count: translators.length })}
|
||||
recorders={translators}
|
||||
/>
|
||||
)}
|
||||
|
||||
{proofreaders.length > 0 && (
|
||||
<RecorderChips
|
||||
title={format("proofreader", { count: proofreaders.length })}
|
||||
recorders={proofreaders}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dubbers.length > 0 && (
|
||||
<RecorderChips title={format("dubber", { count: dubbers.length })} recorders={dubbers} />
|
||||
)}
|
||||
|
||||
{subbers.length > 0 && (
|
||||
<RecorderChips title={format("subber", { count: subbers.length })} recorders={subbers} />
|
||||
)}
|
||||
|
||||
{authors.length > 0 && (
|
||||
<RecorderChips title={format("author", { count: authors.length })} recorders={authors} />
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(notes) && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{format("notes")}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
<Markdawn text={notes} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RecorderChipsProps {
|
||||
title: string;
|
||||
recorders: { attributes?: { username: string } | null }[];
|
||||
}
|
||||
|
||||
const RecorderChips = ({ title, recorders }: RecorderChipsProps) => (
|
||||
<div className="flex flex-wrap place-content-center place-items-center gap-1">
|
||||
<p className="pr-1 font-headers font-bold">{title}:</p>
|
||||
{filterHasAttributes(recorders, ["attributes"]).map((recorder) => (
|
||||
<RecorderChip key={recorder.attributes.username} username={recorder.attributes.username} />
|
||||
))}
|
||||
</div>
|
||||
);
|
|
@ -4,7 +4,7 @@ import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img";
|
|||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props
|
||||
|
|
|
@ -20,13 +20,12 @@ interface Props {
|
|||
icon?: MaterialSymbol;
|
||||
text?: string | null | undefined;
|
||||
alwaysNewTab?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseUp?: MouseEventHandler<HTMLButtonElement>;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseUp?: MouseEventHandler<HTMLDivElement>;
|
||||
draggable?: boolean;
|
||||
badgeNumber?: number;
|
||||
disabled?: boolean;
|
||||
size?: "normal" | "small";
|
||||
type?: "button" | "reset" | "submit";
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
@ -44,31 +43,31 @@ export const Button = ({
|
|||
alwaysNewTab = false,
|
||||
badgeNumber,
|
||||
disabled,
|
||||
type,
|
||||
size = "normal",
|
||||
}: Props): JSX.Element => (
|
||||
<Link href={href} alwaysNewTab={alwaysNewTab} disabled={disabled}>
|
||||
<div className="relative">
|
||||
<button
|
||||
type={type}
|
||||
<div
|
||||
draggable={draggable}
|
||||
id={id}
|
||||
disabled={disabled}
|
||||
onClick={(event) => onClick?.(event)}
|
||||
onClick={(event) => !disabled && onClick?.(event)}
|
||||
onMouseUp={onMouseUp}
|
||||
onFocus={(event) => event.target.blur()}
|
||||
className={cJoin(
|
||||
`group grid w-full grid-flow-col
|
||||
place-content-center place-items-center gap-2 rounded-full border
|
||||
border-dark leading-none text-dark transition-all
|
||||
disabled:cursor-not-allowed disabled:opacity-50 disabled:grayscale`,
|
||||
cIf(size === "small", "h-6 px-3 py-1 text-xs", "h-10 px-4 py-3"),
|
||||
cIf(active, "!border-black bg-black !text-light shadow-lg shadow-black"),
|
||||
`group grid cursor-pointer select-none grid-flow-col place-content-center
|
||||
place-items-center gap-2 rounded-full border border-dark
|
||||
leading-none text-dark transition-all`,
|
||||
cIf(size === "small", "px-3 py-1 text-xs", "px-4 py-3"),
|
||||
cIf(active, "!border-black bg-black !text-light drop-shadow-lg shadow-black"),
|
||||
cIf(
|
||||
!disabled && !active,
|
||||
`shadow-shade hover:bg-dark hover:text-light hover:shadow-lg hover:shadow-shade
|
||||
active:hover:!border-black active:hover:bg-black active:hover:!text-light
|
||||
active:hover:shadow-lg active:hover:shadow-black`
|
||||
disabled,
|
||||
"cursor-not-allowed opacity-50 grayscale",
|
||||
cIf(
|
||||
!active,
|
||||
`shadow-shade hover:bg-dark hover:text-light hover:drop-shadow-lg
|
||||
active:hover:!border-black active:hover:bg-black active:hover:!text-light
|
||||
active:hover:drop-shadow-lg active:hover:shadow-black`
|
||||
)
|
||||
),
|
||||
className
|
||||
)}>
|
||||
|
@ -91,10 +90,8 @@ export const Button = ({
|
|||
weight={size === "normal" ? 500 : 800}
|
||||
/>
|
||||
)}
|
||||
{isDefinedAndNotEmpty(text) && (
|
||||
<p className="line-clamp-1 -translate-y-[0.05em] text-center leading-5">{text}</p>
|
||||
)}
|
||||
</button>
|
||||
{isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Placement } from "tippy.js";
|
||||
import { Button } from "./Button";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
|
||||
|
@ -10,14 +10,9 @@ import { isDefined } from "helpers/asserts";
|
|||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
type ButtonProps = Parameters<typeof Button>[0];
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
interface Props {
|
||||
className?: string;
|
||||
vertical?: boolean;
|
||||
size?: ButtonProps["size"];
|
||||
buttonsProps: (Omit<ButtonProps, "size"> & {
|
||||
visible?: boolean;
|
||||
buttonsProps: (Parameters<typeof Button>[0] & {
|
||||
tooltip?: React.ReactNode | null | undefined;
|
||||
tooltipPlacement?: Placement;
|
||||
})[];
|
||||
|
@ -25,74 +20,29 @@ export interface ButtonGroupProps {
|
|||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const ButtonGroup = ({
|
||||
buttonsProps,
|
||||
className,
|
||||
vertical,
|
||||
size,
|
||||
}: ButtonGroupProps): JSX.Element => (
|
||||
<FilteredButtonGroup
|
||||
buttonsProps={buttonsProps.filter((button) => button.visible !== false)}
|
||||
className={className}
|
||||
vertical={vertical}
|
||||
size={size}
|
||||
/>
|
||||
export const ButtonGroup = ({ buttonsProps, className }: Props): JSX.Element => (
|
||||
<div className={cJoin("grid grid-flow-col", className)}>
|
||||
{buttonsProps.map((buttonProps, index) => (
|
||||
<ConditionalWrapper
|
||||
key={index}
|
||||
isWrapping={isDefined(buttonProps.tooltip)}
|
||||
wrapper={ToolTipWrapper}
|
||||
wrapperProps={{ text: buttonProps.tooltip ?? "", placement: buttonProps.tooltipPlacement }}>
|
||||
<Button
|
||||
{...buttonProps}
|
||||
className={
|
||||
index === 0
|
||||
? "rounded-r-none border-r-0"
|
||||
: index === buttonsProps.length - 1
|
||||
? "rounded-l-none"
|
||||
: "rounded-none border-r-0"
|
||||
}
|
||||
/>
|
||||
</ConditionalWrapper>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const FilteredButtonGroup = ({
|
||||
buttonsProps,
|
||||
className,
|
||||
vertical = false,
|
||||
size = "normal",
|
||||
}: ButtonGroupProps) => {
|
||||
const firstClassName = cIf(
|
||||
vertical,
|
||||
cJoin("rounded-b-none border-b-0", cIf(size === "normal", "rounded-t-3xl", "rounded-t-xl")),
|
||||
"rounded-r-none border-r-0"
|
||||
);
|
||||
|
||||
const lastClassName = cIf(
|
||||
vertical,
|
||||
cJoin("rounded-t-none border-t-0", cIf(size === "normal", "rounded-b-3xl", "rounded-b-xl")),
|
||||
"rounded-l-none border-l-0"
|
||||
);
|
||||
|
||||
const middleClassName = cIf(vertical, "rounded-none border-y-0", "rounded-none border-x-0");
|
||||
|
||||
return (
|
||||
<div className={cJoin("grid", cIf(!vertical, "grid-flow-col"), className)}>
|
||||
{buttonsProps.map((buttonProps, index) => (
|
||||
<ConditionalWrapper
|
||||
key={index}
|
||||
isWrapping={isDefined(buttonProps.tooltip)}
|
||||
wrapper={ToolTipWrapper}
|
||||
wrapperProps={{
|
||||
text: buttonProps.tooltip ?? "",
|
||||
placement: buttonProps.tooltipPlacement,
|
||||
}}>
|
||||
<Button
|
||||
{...buttonProps}
|
||||
size={size}
|
||||
className={cJoin(
|
||||
"relative",
|
||||
cIf(
|
||||
vertical && buttonProps.active && index < buttonsProps.length - 1,
|
||||
"shadow-black/60"
|
||||
),
|
||||
cIf(buttonProps.active, "z-10", "z-0"),
|
||||
index === 0
|
||||
? firstClassName
|
||||
: index === buttonsProps.length - 1
|
||||
? lastClassName
|
||||
: middleClassName
|
||||
)}
|
||||
/>
|
||||
</ConditionalWrapper>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
|
|
|
@ -2,9 +2,11 @@ import { Fragment } from "react";
|
|||
import { ToolTip } from "../ToolTip";
|
||||
import { Button } from "./Button";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { prettyLanguage } from "helpers/formatters";
|
||||
import { iterateMap } from "helpers/others";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -30,7 +32,7 @@ export const LanguageSwitcher = ({
|
|||
onLanguageChanged,
|
||||
showBadge = true,
|
||||
}: Props): JSX.Element => {
|
||||
const { formatLanguage } = useFormat();
|
||||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
return (
|
||||
<ToolTip
|
||||
content={
|
||||
|
@ -43,7 +45,7 @@ export const LanguageSwitcher = ({
|
|||
onLanguageChanged(value);
|
||||
sendAnalytics("Language Switcher", `Switch language (${locale})`);
|
||||
}}
|
||||
text={formatLanguage(locale)}
|
||||
text={prettyLanguage(locale, languages)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
|
|
@ -52,7 +52,7 @@ export const Select = ({
|
|||
ref={ref}
|
||||
className={cJoin(
|
||||
"relative text-center transition-filter",
|
||||
cIf(isOpened, "z-20 drop-shadow-lg shadow-shade"),
|
||||
cIf(isOpened, "z-10 drop-shadow-lg shadow-shade"),
|
||||
className
|
||||
)}>
|
||||
<div
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { forwardRef } from "react";
|
||||
import { Ico } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
|
@ -19,32 +18,34 @@ interface Props {
|
|||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, Props>(
|
||||
({ value, onChange, className, name, placeholder, disabled = false }, ref) => (
|
||||
<div className={cJoin("relative", className)}>
|
||||
<input
|
||||
ref={ref}
|
||||
className="w-full"
|
||||
type="text"
|
||||
name={name}
|
||||
autoCapitalize="off"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder ?? undefined}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{isDefinedAndNotEmpty(value) && (
|
||||
<div className="absolute bottom-0 right-4 top-0 grid place-items-center">
|
||||
<Ico
|
||||
className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))}
|
||||
icon="close"
|
||||
onClick={() => !disabled && onChange("")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
export const TextInput = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
name,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element => (
|
||||
<div className={cJoin("relative", className)}>
|
||||
<input
|
||||
className="w-full"
|
||||
type="text"
|
||||
name={name}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder ?? undefined}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{isDefinedAndNotEmpty(value) && (
|
||||
<div className="absolute bottom-0 right-4 top-0 grid place-items-center">
|
||||
<Ico
|
||||
className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))}
|
||||
icon="close"
|
||||
onClick={() => !disabled && onChange("")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
TextInput.displayName = "TextInput";
|
||||
|
|
|
@ -39,7 +39,6 @@ export const LightBox = ({
|
|||
onPressNext,
|
||||
}: Props): JSX.Element => {
|
||||
const [currentZoom, setCurrentZoom] = useState(1);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const { isFullscreen, toggleFullscreen, exitFullscreen, requestFullscreen } = useFullscreen(
|
||||
Ids.LightBox
|
||||
);
|
||||
|
@ -63,7 +62,7 @@ export const LightBox = ({
|
|||
id={Ids.LightBox}
|
||||
className={cJoin(
|
||||
"fixed inset-0 z-50 grid place-content-center transition-filter duration-500",
|
||||
cIf(isVisible, cIf(!isPerfModeEnabled, "backdrop-blur"), "pointer-events-none touch-none")
|
||||
cIf(isVisible, "backdrop-blur", "pointer-events-none touch-none")
|
||||
)}>
|
||||
<div
|
||||
className={cJoin(
|
||||
|
@ -91,10 +90,8 @@ export const LightBox = ({
|
|||
}}>
|
||||
{isDefined(src) && (
|
||||
<Img
|
||||
className={cJoin(
|
||||
`h-[calc(100vh-4rem)] w-full object-contain`,
|
||||
cIf(!isPerfModeEnabled, "drop-shadow-2xl shadow-shade")
|
||||
)}
|
||||
className={`h-[calc(100vh-4rem)] w-full object-contain drop-shadow-2xl
|
||||
shadow-shade`}
|
||||
src={src}
|
||||
quality={ImageQuality.Large}
|
||||
/>
|
||||
|
|
|
@ -17,8 +17,6 @@ import { atoms } from "contexts/atoms";
|
|||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { VideoPlayer } from "components/Player";
|
||||
import { getVideoFile } from "helpers/videos";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -146,20 +144,6 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
},
|
||||
},
|
||||
|
||||
Angelic: {
|
||||
component: (comProps) => <span className="font-angelic">{comProps.children}</span>,
|
||||
},
|
||||
|
||||
Video: {
|
||||
component: (comProps) => (
|
||||
<VideoPlayer
|
||||
src={getVideoFile(comProps.id)}
|
||||
title={comProps.title}
|
||||
className="my-8"
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
InsetBox: {
|
||||
component: (compProps) => <InsetBox className="my-12">{compProps.children}</InsetBox>,
|
||||
},
|
||||
|
|
|
@ -2,8 +2,10 @@ import { useCallback } from "react";
|
|||
import { Button } from "components/Inputs/Button";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { isUndefined } from "helpers/asserts";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -13,18 +15,27 @@ import { cJoin } from "helpers/className";
|
|||
interface Props {
|
||||
href: string;
|
||||
title: string | null | undefined;
|
||||
|
||||
displayOnlyOn?: "1ColumnLayout" | "3ColumnsLayout";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const ReturnButton = ({ href, title, className }: Props): JSX.Element => {
|
||||
export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
|
||||
return (
|
||||
<div className={cJoin("mx-auto w-full max-w-lg place-self-center", className)}>
|
||||
<Button href={href} text={format("return_to_x", { x: title })} icon="navigate_before" />
|
||||
</div>
|
||||
<>
|
||||
{((is3ColumnsLayout && displayOnlyOn === "3ColumnsLayout") ||
|
||||
(!is3ColumnsLayout && displayOnlyOn === "1ColumnLayout") ||
|
||||
isUndefined(displayOnlyOn)) && (
|
||||
<div className={className}>
|
||||
<Button href={href} text={format("return_to_x", { x: title })} icon="navigate_before" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import { Popup } from "components/Containers/Popup";
|
||||
import { Ico } from "components/Ico";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
export const DebugPopup = (): JSX.Element => {
|
||||
const [isDebugMenuOpened, setDebugMenuOpened] = useAtomPair(atoms.layout.debugMenuOpened);
|
||||
|
||||
const os = useAtomGetter(atoms.userAgent.os);
|
||||
const browser = useAtomGetter(atoms.userAgent.browser);
|
||||
const engine = useAtomGetter(atoms.userAgent.engine);
|
||||
const deviceType = useAtomGetter(atoms.userAgent.deviceType);
|
||||
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const isPerfModeToggleable = useAtomGetter(atoms.settings.isPerfModeToggleable);
|
||||
const perfMode = useAtomGetter(atoms.settings.perfMode);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
isVisible={isDebugMenuOpened}
|
||||
onCloseRequest={() => {
|
||||
setDebugMenuOpened(false);
|
||||
sendAnalytics("Debug", "Close debug menu");
|
||||
}}>
|
||||
<h2 className="inline-flex place-items-center gap-2 text-2xl">
|
||||
<Ico icon="bug_report" isFilled />
|
||||
Debug Menu
|
||||
</h2>
|
||||
|
||||
<h3>User Agent</h3>
|
||||
<div>
|
||||
<p>OS: {os}</p>
|
||||
<p>Device type: {deviceType ?? "undefined"}</p>
|
||||
<p>Browser: {browser}</p>
|
||||
<p>Engine: {engine}</p>
|
||||
</div>
|
||||
|
||||
<h3>Settings</h3>
|
||||
<div>
|
||||
<p>Raw perf mode: {perfMode}</p>
|
||||
<p>Perf mode: {isPerfModeEnabled ? "true" : "false"}</p>
|
||||
<p>Perf mode toggleable: {isPerfModeToggleable ? "true" : "false"}</p>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
|
@ -23,12 +23,9 @@ export const MainPanel = (): JSX.Element => {
|
|||
const { format } = useFormat();
|
||||
const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced);
|
||||
const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened);
|
||||
const [isSettingsOpened, setSettingsOpened] = useAtomPair(atoms.layout.settingsOpened);
|
||||
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
|
||||
const [isDebugMenuOpened, setDebugMenuOpened] = useAtomPair(atoms.layout.debugMenuOpened);
|
||||
const isDebugMenuAvailable = useAtomGetter(atoms.layout.debugMenuAvailable);
|
||||
|
||||
const closeMainPanel = useCallback(() => setMainPanelOpened(false), [setMainPanelOpened]);
|
||||
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
|
||||
const setSearchOpened = useAtomSetter(atoms.layout.searchOpened);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -85,7 +82,6 @@ export const MainPanel = (): JSX.Element => {
|
|||
content={<h3 className="text-2xl">{format("open_settings")}</h3>}
|
||||
placement={isMainPanelReduced ? "right" : "top"}>
|
||||
<Button
|
||||
active={isSettingsOpened}
|
||||
onClick={() => {
|
||||
closeMainPanel();
|
||||
setSettingsOpened(true);
|
||||
|
@ -98,7 +94,6 @@ export const MainPanel = (): JSX.Element => {
|
|||
content={<h3 className="text-2xl">{format("open_search")}</h3>}
|
||||
placement={isMainPanelReduced ? "right" : "top"}>
|
||||
<Button
|
||||
active={isSearchOpened}
|
||||
onClick={() => {
|
||||
closeMainPanel();
|
||||
setSearchOpened(true);
|
||||
|
@ -107,21 +102,6 @@ export const MainPanel = (): JSX.Element => {
|
|||
icon="search"
|
||||
/>
|
||||
</ToolTip>
|
||||
{isDebugMenuAvailable && (
|
||||
<ToolTip
|
||||
content={<h3 className="text-2xl">Debug menu</h3>}
|
||||
placement={isMainPanelReduced ? "right" : "top"}>
|
||||
<Button
|
||||
active={isDebugMenuOpened}
|
||||
onClick={() => {
|
||||
closeMainPanel();
|
||||
setDebugMenuOpened(true);
|
||||
sendAnalytics("Debug", "Open debug menu");
|
||||
}}
|
||||
icon="bug_report"
|
||||
/>
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { Popup } from "components/Containers/Popup";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
} from "shared/meilisearch-graphql-typings/meiliTypes";
|
||||
import { getVideoThumbnailURL } from "helpers/videos";
|
||||
import { UpPressable } from "components/Containers/UpPressable";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { prettyItemSubType, prettySlug } from "helpers/formatters";
|
||||
import { Ico } from "components/Ico";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
|
@ -52,23 +52,16 @@ interface MultiResult {
|
|||
export const SearchPopup = (): JSX.Element => {
|
||||
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
|
||||
const [query, setQuery] = useState("");
|
||||
const {
|
||||
format,
|
||||
formatCategory,
|
||||
formatContentType,
|
||||
formatWikiTag,
|
||||
formatLibraryItemSubType,
|
||||
formatWeaponType,
|
||||
} = useFormat();
|
||||
const { format } = useFormat();
|
||||
const [multiResult, setMultiResult] = useState<MultiResult>({});
|
||||
|
||||
const fetchSearchResults = useCallback((q: string) => {
|
||||
useEffect(() => {
|
||||
const fetchMultiResult = async () => {
|
||||
const searchResults = (
|
||||
await meiliMultiSearch([
|
||||
{
|
||||
indexUid: MeiliIndices.LIBRARY_ITEM,
|
||||
q,
|
||||
q: query,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToRetrieve: [
|
||||
"title",
|
||||
|
@ -87,7 +80,7 @@ export const SearchPopup = (): JSX.Element => {
|
|||
},
|
||||
{
|
||||
indexUid: MeiliIndices.CONTENT,
|
||||
q,
|
||||
q: query,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
|
||||
attributesToHighlight: ["translations"],
|
||||
|
@ -95,7 +88,7 @@ export const SearchPopup = (): JSX.Element => {
|
|||
},
|
||||
{
|
||||
indexUid: MeiliIndices.VIDEOS,
|
||||
q,
|
||||
q: query,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToRetrieve: [
|
||||
"title",
|
||||
|
@ -111,16 +104,20 @@ export const SearchPopup = (): JSX.Element => {
|
|||
},
|
||||
{
|
||||
indexUid: MeiliIndices.POST,
|
||||
q,
|
||||
q: query,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"],
|
||||
attributesToHighlight: ["translations.title", "translations.displayable_description"],
|
||||
attributesToCrop: ["translations.displayable_description"],
|
||||
attributesToHighlight: [
|
||||
"translations.title",
|
||||
"translations.excerpt",
|
||||
"translations.body",
|
||||
],
|
||||
attributesToCrop: ["translations.body"],
|
||||
filter: ["hidden = false"],
|
||||
},
|
||||
{
|
||||
indexUid: MeiliIndices.WEAPON,
|
||||
q,
|
||||
q: query,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToHighlight: ["translations.description", "translations.names"],
|
||||
attributesToCrop: ["translations.description"],
|
||||
|
@ -128,7 +125,7 @@ export const SearchPopup = (): JSX.Element => {
|
|||
},
|
||||
{
|
||||
indexUid: MeiliIndices.WIKI_PAGE,
|
||||
q,
|
||||
q: query,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToHighlight: [
|
||||
"translations.title",
|
||||
|
@ -187,16 +184,12 @@ export const SearchPopup = (): JSX.Element => {
|
|||
setMultiResult(result);
|
||||
};
|
||||
|
||||
if (q === "") {
|
||||
if (query === "") {
|
||||
setMultiResult({});
|
||||
} else {
|
||||
fetchMultiResult();
|
||||
}
|
||||
|
||||
setQuery(q);
|
||||
}, []);
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
|
@ -205,18 +198,12 @@ export const SearchPopup = (): JSX.Element => {
|
|||
setSearchOpened(false);
|
||||
sendAnalytics("Search", "Close search");
|
||||
}}
|
||||
onOpen={() => searchInputRef.current?.focus()}
|
||||
fillViewport>
|
||||
<h2 className="inline-flex place-items-center gap-2 text-2xl">
|
||||
<Ico icon="search" isFilled />
|
||||
{format("search")}
|
||||
</h2>
|
||||
<TextInput
|
||||
ref={searchInputRef}
|
||||
onChange={fetchSearchResults}
|
||||
value={query}
|
||||
placeholder={format("search_placeholder")}
|
||||
/>
|
||||
<TextInput onChange={setQuery} value={query} placeholder={format("search_title")} />
|
||||
|
||||
<div className="flex w-full flex-wrap gap-12 gap-x-16">
|
||||
{isDefined(multiResult.libraryItems) && (
|
||||
|
@ -250,11 +237,11 @@ export const SearchPopup = (): JSX.Element => {
|
|||
keepInfoVisible
|
||||
topChips={
|
||||
item.metadata && item.metadata.length > 0 && item.metadata[0]
|
||||
? [formatLibraryItemSubType(item.metadata[0])]
|
||||
? [prettyItemSubType(item.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
bottomChips={item.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.release_date,
|
||||
|
@ -295,11 +282,15 @@ export const SearchPopup = (): JSX.Element => {
|
|||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.type?.data?.attributes
|
||||
? [formatContentType(item.type.data.attributes.slug)]
|
||||
? [
|
||||
item.type.data.attributes.titles?.[0]
|
||||
? item.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
bottomChips={item.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
|
@ -348,11 +339,11 @@ export const SearchPopup = (): JSX.Element => {
|
|||
thumbnailRounded
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible
|
||||
topChips={filterHasAttributes(item.tags?.data, ["attributes"]).map((tag) =>
|
||||
formatWikiTag(tag.attributes.slug)
|
||||
topChips={filterHasAttributes(item.tags?.data, ["attributes"]).map(
|
||||
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
|
||||
)}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
(category) => category.attributes.short
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
@ -375,10 +366,12 @@ export const SearchPopup = (): JSX.Element => {
|
|||
onClick={() => setSearchOpened(false)}
|
||||
translations={filterHasAttributes(item._formatted.translations, [
|
||||
"language.data.attributes.code",
|
||||
]).map(({ excerpt, displayable_description, language, ...otherAttributes }) => ({
|
||||
]).map(({ excerpt, body, language, ...otherAttributes }) => ({
|
||||
...otherAttributes,
|
||||
description: containsHighlight(displayable_description)
|
||||
? displayable_description
|
||||
description: containsHighlight(excerpt)
|
||||
? excerpt
|
||||
: containsHighlight(body)
|
||||
? body
|
||||
: excerpt,
|
||||
language: language.data.attributes.code,
|
||||
}))}
|
||||
|
@ -387,8 +380,8 @@ export const SearchPopup = (): JSX.Element => {
|
|||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
bottomChips={item.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.date,
|
||||
|
@ -469,11 +462,11 @@ export const SearchPopup = (): JSX.Element => {
|
|||
keepInfoVisible
|
||||
topChips={
|
||||
item.type?.data?.attributes?.slug
|
||||
? [formatWeaponType(item.type.data.attributes.slug)]
|
||||
? [prettySlug(item.type.data.attributes.slug)]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
bottomChips={filterHasAttributes(item.categories, ["attributes.short"]).map(
|
||||
(category) => category.attributes.short
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||
import { OrderableList } from "components/Inputs/OrderableList";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
|
@ -7,14 +8,14 @@ import { TextInput } from "components/Inputs/TextInput";
|
|||
import { Popup } from "components/Containers/Popup";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { cJoin, cIf } from "helpers/className";
|
||||
import { prettyLanguage } from "helpers/formatters";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||
import { PerfMode, ThemeMode } from "contexts/settings";
|
||||
import { useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
import { ThemeMode } from "contexts/settings";
|
||||
import { Ico } from "components/Ico";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -31,11 +32,9 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
const [fontSize, setFontSize] = useAtomPair(atoms.settings.fontSize);
|
||||
const [playerName, setPlayerName] = useAtomPair(atoms.settings.playerName);
|
||||
const [themeMode, setThemeMode] = useAtomPair(atoms.settings.themeMode);
|
||||
const setPerfMode = useAtomSetter(atoms.settings.perfMode);
|
||||
const perfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const isPerfModeToggleable = useAtomGetter(atoms.settings.isPerfModeToggleable);
|
||||
|
||||
const { format, formatLanguage } = useFormat();
|
||||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
const { format } = useFormat();
|
||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
@ -75,7 +74,7 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
<OrderableList
|
||||
items={preferredLanguages.map((locale) => ({
|
||||
code: locale,
|
||||
name: formatLanguage(locale),
|
||||
name: prettyLanguage(locale, languages),
|
||||
}))}
|
||||
insertLabels={[
|
||||
{
|
||||
|
@ -200,28 +199,23 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
<div>
|
||||
<h3 className="text-xl">{format("font")}</h3>
|
||||
<div className="grid gap-2">
|
||||
<ButtonGroup
|
||||
vertical
|
||||
buttonsProps={[
|
||||
{
|
||||
active: !isDyslexic,
|
||||
onClick: () => {
|
||||
setDyslexic(false);
|
||||
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
||||
},
|
||||
className: "font-zenMaruGothic",
|
||||
text: "Zen Maru Gothic",
|
||||
},
|
||||
{
|
||||
active: isDyslexic,
|
||||
onClick: () => {
|
||||
setDyslexic(true);
|
||||
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
||||
},
|
||||
className: "font-openDyslexic",
|
||||
text: "OpenDyslexic",
|
||||
},
|
||||
]}
|
||||
<Button
|
||||
active={!isDyslexic}
|
||||
onClick={() => {
|
||||
setDyslexic(false);
|
||||
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
||||
}}
|
||||
className="font-zenMaruGothic"
|
||||
text="Zen Maru Gothic"
|
||||
/>
|
||||
<Button
|
||||
active={isDyslexic}
|
||||
onClick={() => {
|
||||
setDyslexic(true);
|
||||
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
||||
}}
|
||||
className="font-openDyslexic"
|
||||
text="OpenDyslexic"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -243,20 +237,6 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<div className="flex place-content-center place-items-center gap-1">
|
||||
<h3 className="text-xl">{format("performance_mode")}</h3>
|
||||
<ToolTip content={format("performance_mode_tooltip")} placement="top">
|
||||
<Ico icon="info" />
|
||||
</ToolTip>
|
||||
</div>
|
||||
<Switch
|
||||
value={perfModeEnabled}
|
||||
onClick={() => setPerfMode(perfModeEnabled ? PerfMode.Off : PerfMode.On)}
|
||||
disabled={!isPerfModeToggleable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
|
|
@ -1,301 +0,0 @@
|
|||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
import Slider from "rc-slider";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { prettyDuration } from "helpers/formatters";
|
||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { useFullscreen } from "hooks/useFullscreen";
|
||||
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
const STEP_MULTIPLIER = 100;
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface AudioPlayerProps {
|
||||
src?: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const AudioPlayer = ({ src, className, title }: AudioPlayerProps): JSX.Element => {
|
||||
const [ref, setRef] = useState<HTMLAudioElement | null>(null);
|
||||
const [isFocused, setFocus] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cJoin("w-full", className)}
|
||||
tabIndex={0}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}>
|
||||
<audio ref={setRef} src={src} />
|
||||
{ref && (
|
||||
<PlayerControls
|
||||
className={className}
|
||||
mediaRef={ref}
|
||||
type="audio"
|
||||
src={src}
|
||||
title={title}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src?: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
rounded?: boolean;
|
||||
subSrc?: string;
|
||||
}
|
||||
|
||||
export const VideoPlayer = ({
|
||||
src,
|
||||
className,
|
||||
title,
|
||||
subSrc,
|
||||
rounded = true,
|
||||
}: VideoPlayerProps): JSX.Element => {
|
||||
const [ref, setRef] = useState<HTMLVideoElement | null>(null);
|
||||
const videoId = useId();
|
||||
const { isFullscreen, toggleFullscreen } = useFullscreen(videoId);
|
||||
const [isPlaying, setPlaying] = useState(false);
|
||||
const [isFocused, setFocus] = useState(false);
|
||||
|
||||
const togglePlayback = useCallback(
|
||||
async () => (isPlaying ? ref?.pause() : await ref?.play()),
|
||||
[isPlaying, ref]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cJoin("grid w-full", className)}
|
||||
id={videoId}
|
||||
tabIndex={0}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}>
|
||||
<video
|
||||
ref={setRef}
|
||||
className={cJoin("h-full w-full", cIf(!isFullscreen && rounded, "rounded-t-4xl"))}
|
||||
crossOrigin="anonymous"
|
||||
onClick={togglePlayback}
|
||||
onDoubleClick={toggleFullscreen}>
|
||||
<source type="video/mp4" src={src} />
|
||||
{subSrc && <track label="English" kind="subtitles" srcLang="en" src={subSrc} default />}
|
||||
</video>
|
||||
{ref && (
|
||||
<PlayerControls
|
||||
title={title}
|
||||
mediaRef={ref}
|
||||
src={src}
|
||||
type="video"
|
||||
className={cIf(isFullscreen || !rounded, "rounded-none", "rounded-b-4xl rounded-t-none")}
|
||||
fullscreen={{ isFullscreen, toggleFullscreen }}
|
||||
onPlaybackChanged={setPlaying}
|
||||
isFocused={isFocused}
|
||||
hasCC={isDefined(subSrc)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface PlayerControls {
|
||||
mediaRef: HTMLMediaElement;
|
||||
src?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
isFocused?: boolean;
|
||||
type: "audio" | "video";
|
||||
fullscreen?: {
|
||||
isFullscreen: boolean;
|
||||
toggleFullscreen: () => void;
|
||||
};
|
||||
onPlaybackChanged?: (isPlaying: boolean) => void;
|
||||
hasCC?: boolean;
|
||||
}
|
||||
|
||||
const PlayerControls = ({
|
||||
mediaRef,
|
||||
className,
|
||||
src,
|
||||
title,
|
||||
fullscreen,
|
||||
isFocused = false,
|
||||
hasCC = false,
|
||||
type,
|
||||
onPlaybackChanged,
|
||||
}: PlayerControls) => {
|
||||
const [isPlaying, setPlaying] = useState(false);
|
||||
const [duration, setDuration] = useState(mediaRef.duration);
|
||||
const [currentTime, setCurrentTime] = useState(mediaRef.currentTime);
|
||||
const [isMuted, setMuted] = useState(mediaRef.volume === 0);
|
||||
const [hasEnded, setEnded] = useState(false);
|
||||
const [ccVisible, setCCVisible] = useState(hasCC);
|
||||
const isContentPanelAtLeastXl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastXl);
|
||||
const isContentPanelAtLeastMd = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastMd);
|
||||
|
||||
const togglePlayback = useCallback(
|
||||
async () => (isPlaying ? mediaRef.pause() : await mediaRef.play()),
|
||||
[isPlaying, mediaRef]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"left",
|
||||
() => {
|
||||
mediaRef.currentTime -= 5;
|
||||
},
|
||||
{ enabled: isFocused }
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"right",
|
||||
() => {
|
||||
mediaRef.currentTime += 5;
|
||||
},
|
||||
{ enabled: isFocused }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = mediaRef;
|
||||
audio.addEventListener("loadedmetadata", () => {
|
||||
setDuration(audio.duration);
|
||||
});
|
||||
|
||||
audio.addEventListener("play", () => {
|
||||
setPlaying(true);
|
||||
onPlaybackChanged?.(true);
|
||||
setEnded(false);
|
||||
});
|
||||
audio.addEventListener("pause", () => {
|
||||
setPlaying(false);
|
||||
onPlaybackChanged?.(false);
|
||||
});
|
||||
|
||||
audio.addEventListener("ended", () => setEnded(true));
|
||||
|
||||
audio.addEventListener("timeupdate", () => {
|
||||
setCurrentTime(audio.currentTime);
|
||||
});
|
||||
|
||||
return () => audio.pause();
|
||||
}, [mediaRef, onPlaybackChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const textTrack = mediaRef.textTracks[0];
|
||||
if (isUndefined(textTrack)) return;
|
||||
textTrack.mode = ccVisible ? "showing" : "hidden";
|
||||
}, [ccVisible, mediaRef.textTracks]);
|
||||
|
||||
const buttonGroup = (
|
||||
<ButtonGroup
|
||||
vertical={!isContentPanelAtLeastXl && type === "video"}
|
||||
buttonsProps={[
|
||||
{
|
||||
icon: isMuted ? "volume_off" : "volume_up",
|
||||
active: isMuted,
|
||||
onClick: () => {
|
||||
setMuted((oldMutedValue) => {
|
||||
const newMutedValue = !oldMutedValue;
|
||||
mediaRef.volume = newMutedValue ? 0 : 1;
|
||||
return newMutedValue;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "closed_caption",
|
||||
active: ccVisible,
|
||||
onClick: () => setCCVisible((value) => !value),
|
||||
visible: hasCC,
|
||||
},
|
||||
{
|
||||
icon: fullscreen?.isFullscreen ? "fullscreen_exit" : "fullscreen",
|
||||
active: fullscreen?.isFullscreen,
|
||||
onClick: fullscreen?.toggleFullscreen,
|
||||
visible: isDefined(fullscreen),
|
||||
},
|
||||
{ icon: "download", href: src, alwaysNewTab: true },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cJoin(
|
||||
`relative flex w-full place-items-center rounded-full
|
||||
bg-highlight p-3 shadow-md shadow-shade/50`,
|
||||
cIf(isContentPanelAtLeastMd, "gap-5", "gap-3"),
|
||||
className
|
||||
)}>
|
||||
<Button
|
||||
icon={hasEnded ? "replay" : isPlaying ? "pause" : "play_arrow"}
|
||||
active={isPlaying}
|
||||
onClick={togglePlayback}
|
||||
/>
|
||||
<div className="grid w-full place-items-start">
|
||||
{isDefinedAndNotEmpty(title) && (
|
||||
<p className="!my-0 line-clamp-1 text-left text-xs text-dark">{title}</p>
|
||||
)}
|
||||
<div
|
||||
className={cJoin(
|
||||
"flex w-full place-content-between place-items-center",
|
||||
cIf(isContentPanelAtLeastMd, "gap-5", "gap-3")
|
||||
)}>
|
||||
<p
|
||||
className={cJoin(
|
||||
"!my-0 font-mono",
|
||||
cIf(isContentPanelAtLeastMd, "h-5", "h-3 text-xs")
|
||||
)}>
|
||||
{prettyDuration(currentTime)}
|
||||
</p>
|
||||
<Slider
|
||||
className={cIf(
|
||||
!isContentPanelAtLeastXl && type === "video",
|
||||
"!absolute left-0 right-0 top-[-5px]"
|
||||
)}
|
||||
value={currentTime * STEP_MULTIPLIER}
|
||||
onChange={(value) => {
|
||||
const newTime = (value as number) / STEP_MULTIPLIER;
|
||||
mediaRef.currentTime = newTime;
|
||||
setCurrentTime(newTime);
|
||||
}}
|
||||
onAfterChange={async () => await mediaRef.play()}
|
||||
max={duration * STEP_MULTIPLIER}
|
||||
/>
|
||||
{!isContentPanelAtLeastXl && type === "video" && <p>/</p>}
|
||||
<p
|
||||
className={cJoin(
|
||||
"!my-0 font-mono",
|
||||
cIf(isContentPanelAtLeastMd, "h-5", "h-3 text-xs")
|
||||
)}>
|
||||
{prettyDuration(duration)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{isContentPanelAtLeastXl ? (
|
||||
buttonGroup
|
||||
) : (
|
||||
<ToolTip content={buttonGroup}>
|
||||
<Button icon="more_vert" />
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,20 +1,22 @@
|
|||
import { useCallback } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
||||
import { Chip } from "./Chip";
|
||||
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
||||
import { ReturnButton } from "./PanelComponents/ReturnButton";
|
||||
import { ContentPanel } from "./Containers/ContentPanel";
|
||||
import { SubPanel } from "./Containers/SubPanel";
|
||||
import { RecorderChip } from "./RecorderChip";
|
||||
import { ThumbnailHeader } from "./ThumbnailHeader";
|
||||
import { ToolTip } from "./ToolTip";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { PostWithTranslations } from "types/types";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { ElementsSeparator } from "helpers/component";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Credits } from "components/Credits";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -49,7 +51,7 @@ export const PostPage = ({
|
|||
displayTitle = true,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const { formatCategory } = useFormat();
|
||||
const { format, formatStatusDescription } = useFormat();
|
||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
|
@ -75,7 +77,34 @@ export const PostPage = ({
|
|||
<ReturnButton href={returnHref} title={returnTitle} />
|
||||
),
|
||||
|
||||
displayCredits && <Credits status={selectedTranslation?.status} authors={post.authors?.data} />,
|
||||
displayCredits && (
|
||||
<>
|
||||
{selectedTranslation && (
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{format("status")}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={formatStatusDescription(selectedTranslation.status)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.authors && post.authors.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{"Authors"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(post.authors.data, ["id", "attributes"]).map((author) => (
|
||||
<Fragment key={author.id}>
|
||||
<RecorderChip recorder={author.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
|
||||
displayToc && isDefined(toc) && (
|
||||
<TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />
|
||||
|
@ -91,8 +120,13 @@ export const PostPage = ({
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
{is1ColumnLayout && returnHref && returnTitle && (
|
||||
<ReturnButton href={returnHref} title={returnTitle} className="mb-10" />
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayThumbnailHeader ? (
|
||||
|
@ -101,10 +135,7 @@ export const PostPage = ({
|
|||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
categories={filterHasAttributes(post.categories?.data, ["attributes"]).map((category) =>
|
||||
formatCategory(category.attributes.slug)
|
||||
)}
|
||||
releaseDate={post.date}
|
||||
categories={post.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Markdown } from "./Markdown/Markdown";
|
||||
import { Chip } from "components/Chip";
|
||||
import { Ico } from "components/Ico";
|
||||
|
@ -6,15 +7,13 @@ import { Img } from "components/Img";
|
|||
import { UpPressable } from "components/Containers/UpPressable";
|
||||
import { DatePickerFragment, PricePickerFragment, UploadImageFragment } from "graphql/generated";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { prettyDuration, prettyShortenNumber } from "helpers/formatters";
|
||||
import { prettyDate, prettyDuration, prettyPrice, prettyShortenNumber } from "helpers/formatters";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -78,25 +77,25 @@ export const PreviewCard = ({
|
|||
disabled = false,
|
||||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
const { formatPrice, formatDate } = useFormat();
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const preferredCurrency = useAtomGetter(atoms.settings.currency);
|
||||
const currency = useAtomGetter(atoms.settings.currency);
|
||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||
const isHoverable = useDeviceSupportsHover();
|
||||
const router = useRouter();
|
||||
|
||||
const metadataJSX = (
|
||||
<>
|
||||
{metadata && (isDefined(metadata.releaseDate) || isDefined(metadata.price)) && (
|
||||
{metadata && (metadata.releaseDate || metadata.price) && (
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
||||
{metadata.releaseDate && (
|
||||
<p className="text-sm">
|
||||
<Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{formatDate(metadata.releaseDate)}
|
||||
{prettyDate(metadata.releaseDate, router.locale)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.price && (
|
||||
<p className="justify-self-end text-sm">
|
||||
<Ico icon="shopping_cart" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{formatPrice(metadata.price, preferredCurrency)}
|
||||
{prettyPrice(metadata.price, currencies, currency)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.views && (
|
||||
|
@ -118,7 +117,7 @@ export const PreviewCard = ({
|
|||
|
||||
return (
|
||||
<UpPressable
|
||||
className={cJoin("relative grid items-end text-left", className)}
|
||||
className={cJoin("grid items-end text-left", className)}
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
noBackground
|
||||
|
@ -178,11 +177,11 @@ export const PreviewCard = ({
|
|||
"z-20 grid gap-2 p-4 transition-opacity linearbg-obi",
|
||||
cIf(
|
||||
!keepInfoVisible && isHoverable,
|
||||
`-inset-x-0.5 bottom-2 opacity-0 !shadow-shade
|
||||
`-inset-x-0.5 bottom-2 opacity-0 shadow-shade
|
||||
[border-radius:10%_10%_10%_10%_/_1%_1%_3%_3%]
|
||||
group-hover:opacity-100 hoverable:absolute hoverable:shadow-lg
|
||||
group-hover:opacity-100 hoverable:absolute hoverable:drop-shadow-lg
|
||||
notHoverable:rounded-b-md notHoverable:opacity-100`,
|
||||
cIf(!isPerfModeEnabled, "[border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]")
|
||||
"[border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]"
|
||||
)
|
||||
)}>
|
||||
{metadata?.position === "Top" && metadataJSX}
|
||||
|
@ -205,7 +204,7 @@ export const PreviewCard = ({
|
|||
)}
|
||||
{subtitle && <Markdown text={subtitle} className="leading-none break-words" />}
|
||||
</div>
|
||||
{description && <Markdown text={description} className="overflow-hidden break-words" />}
|
||||
{description && <Markdown text={description} className="break-words" />}
|
||||
{bottomChips && bottomChips.length > 0 && (
|
||||
<div
|
||||
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
||||
|
|
|
@ -3,12 +3,10 @@ import { Img } from "./Img";
|
|||
import { Markdawn } from "./Markdown/Markdawn";
|
||||
import { ToolTip } from "./ToolTip";
|
||||
import { Chip } from "components/Chip";
|
||||
import { RecorderChipFragment } from "graphql/generated";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { filterHasAttributes, isUndefined } from "helpers/asserts";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -16,22 +14,14 @@ import { useSmartLanguage } from "hooks/useSmartLanguage";
|
|||
*/
|
||||
|
||||
interface Props {
|
||||
username: string;
|
||||
className?: string;
|
||||
recorder: RecorderChipFragment;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const RecorderChip = ({ username }: Props): JSX.Element => {
|
||||
export const RecorderChip = ({ recorder }: Props): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const recorders = useAtomGetter(atoms.localData.recorders);
|
||||
const recorder = recorders.find((elem) => elem.attributes?.username === username)?.attributes;
|
||||
|
||||
const [selectedBioTranslation] = useSmartLanguage({
|
||||
items: recorder?.bio ?? [],
|
||||
languageExtractor: (bio) => bio.language?.data?.attributes?.code,
|
||||
});
|
||||
|
||||
if (isUndefined(recorder)) return <></>;
|
||||
|
||||
return (
|
||||
<ToolTip
|
||||
|
@ -65,7 +55,7 @@ export const RecorderChip = ({ username }: Props): JSX.Element => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedBioTranslation?.bio && <Markdawn text={selectedBioTranslation.bio} />}
|
||||
{recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />}
|
||||
</div>
|
||||
}
|
||||
placement="top">
|
||||
|
|
|
@ -2,9 +2,10 @@ import { Chip } from "components/Chip";
|
|||
import { Img } from "components/Img";
|
||||
import { InsetBox } from "components/Containers/InsetBox";
|
||||
import { Markdawn } from "components/Markdown/Markdawn";
|
||||
import { DatePickerFragment, UploadImageFragment } from "graphql/generated";
|
||||
import { prettyInlineTitle, slugify } from "helpers/formatters";
|
||||
import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
|
||||
import { prettyInlineTitle, prettySlug, slugify } from "helpers/formatters";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
@ -19,9 +20,12 @@ interface Props {
|
|||
title: string | null | undefined;
|
||||
subtitle?: string | null | undefined;
|
||||
description?: string | null | undefined;
|
||||
type?: string;
|
||||
categories?: string[];
|
||||
releaseDate?: DatePickerFragment;
|
||||
type?: NonNullable<
|
||||
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
|
||||
>["type"];
|
||||
categories?: NonNullable<
|
||||
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
|
||||
>["categories"];
|
||||
thumbnail?: UploadImageFragment | null | undefined;
|
||||
className?: string;
|
||||
languageSwitcher?: JSX.Element;
|
||||
|
@ -38,10 +42,9 @@ export const ThumbnailHeader = ({
|
|||
categories,
|
||||
description,
|
||||
languageSwitcher,
|
||||
releaseDate,
|
||||
className,
|
||||
}: Props): JSX.Element => {
|
||||
const { format, formatDate } = useFormat();
|
||||
const { format } = useFormat();
|
||||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||
|
||||
return (
|
||||
|
@ -69,30 +72,25 @@ export const ThumbnailHeader = ({
|
|||
</div>
|
||||
|
||||
<div className="flew-wrap flex flex-row place-content-center gap-8">
|
||||
{type && (
|
||||
{type?.data?.attributes && (
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-xl">{format("type", { count: 1 })}</h3>
|
||||
<div className="flex flex-row flex-wrap">
|
||||
<Chip text={type} />
|
||||
<Chip
|
||||
text={
|
||||
type.data.attributes.titles?.[0]?.title ?? prettySlug(type.data.attributes.slug)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{releaseDate && (
|
||||
{categories && categories.data.length > 0 && (
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-xl">{format("release_date")}</h3>
|
||||
<div className="flex flex-row flex-wrap">
|
||||
<Chip text={formatDate(releaseDate)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categories && categories.length > 0 && (
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-xl">{format("category", { count: categories.length })}</h3>
|
||||
<h3 className="text-xl">{format("category", { count: categories.data.length })}</h3>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{categories.map((category) => (
|
||||
<Chip key={category} text={category} />
|
||||
{filterHasAttributes(categories.data, ["attributes", "id"]).map((category) => (
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { containerQueries } from "contexts/containerQueries";
|
||||
import { userAgent } from "contexts/userAgent";
|
||||
import { atomPairing } from "helpers/atoms";
|
||||
import { settings } from "contexts/settings";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { Languages, Currencies, Langui, Recorders, TypesTranslations } from "helpers/localData";
|
||||
import { Languages, Currencies, Langui } from "helpers/localData";
|
||||
|
||||
/* [ LOCAL DATA ATOMS ] */
|
||||
|
||||
|
@ -13,29 +12,12 @@ const languages = atomPairing(atom<Languages>([]));
|
|||
const currencies = atomPairing(atom<Currencies>([]));
|
||||
const langui = atomPairing(atom<Langui>({}));
|
||||
const fallbackLangui = atomPairing(atom<Langui>({}));
|
||||
const recorders = atomPairing(atom<Recorders>([]));
|
||||
const typesTranslations = atomPairing(
|
||||
atom<TypesTranslations>({
|
||||
audioSubtypes: [],
|
||||
categories: [],
|
||||
contentTypes: [],
|
||||
gamePlatforms: [],
|
||||
groupSubtypes: [],
|
||||
metadataTypes: [],
|
||||
textualSubtypes: [],
|
||||
videoSubtypes: [],
|
||||
wikiPagesTags: [],
|
||||
weaponTypes: [],
|
||||
})
|
||||
);
|
||||
|
||||
const localData = {
|
||||
languages: languages[0],
|
||||
currencies: currencies[0],
|
||||
langui: langui[0],
|
||||
fallbackLangui: fallbackLangui[0],
|
||||
recorders: recorders[0],
|
||||
typesTranslations: typesTranslations[0],
|
||||
};
|
||||
|
||||
/* [ LIGHTBOX ATOMS ] */
|
||||
|
@ -58,8 +40,6 @@ const searchOpened = atomPairing(atom(false));
|
|||
const settingsOpened = atomPairing(atom(false));
|
||||
const subPanelOpened = atomPairing(atom(false));
|
||||
const mainPanelOpened = atomPairing(atom(false));
|
||||
const debugMenuOpened = atomPairing(atom(false));
|
||||
const debugMenuAvailable = atom((get) => get(settings.playerName[0]) === "debug");
|
||||
const menuGesturesEnabled = atomPairing(atomWithStorage("isMenuGesturesEnabled", false));
|
||||
const terminalMode = atom((get) => get(settings.playerName[0]) === "root");
|
||||
|
||||
|
@ -71,8 +51,6 @@ const layout = {
|
|||
mainPanelOpened,
|
||||
menuGesturesEnabled,
|
||||
terminalMode,
|
||||
debugMenuAvailable,
|
||||
debugMenuOpened,
|
||||
};
|
||||
|
||||
export const atoms = {
|
||||
|
@ -81,11 +59,10 @@ export const atoms = {
|
|||
localData,
|
||||
lightBox,
|
||||
containerQueries,
|
||||
userAgent,
|
||||
};
|
||||
|
||||
// Do not import outside of the "contexts" folder
|
||||
export const internalAtoms = {
|
||||
lightBox: lightBoxAtom,
|
||||
localData: { languages, currencies, langui, fallbackLangui, recorders, typesTranslations },
|
||||
localData: { languages, currencies, langui, fallbackLangui },
|
||||
};
|
||||
|
|
|
@ -7,17 +7,10 @@ import {
|
|||
LocalDataGetWebsiteInterfacesQuery,
|
||||
LocalDataGetCurrenciesQuery,
|
||||
LocalDataGetLanguagesQuery,
|
||||
LocalDataGetRecordersQuery,
|
||||
} from "graphql/generated";
|
||||
import { LocalDataFile } from "graphql/fetchLocalData";
|
||||
import { internalAtoms } from "contexts/atoms";
|
||||
import {
|
||||
processLanguages,
|
||||
processCurrencies,
|
||||
processLangui,
|
||||
processRecorders,
|
||||
processTypesTranslations,
|
||||
} from "helpers/localData";
|
||||
import { processLanguages, processCurrencies, processLangui } from "helpers/localData";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
|
||||
|
@ -28,8 +21,6 @@ export const useLocalData = (): void => {
|
|||
const setCurrencies = useAtomSetter(internalAtoms.localData.currencies);
|
||||
const setLangui = useAtomSetter(internalAtoms.localData.langui);
|
||||
const setFallbackLangui = useAtomSetter(internalAtoms.localData.fallbackLangui);
|
||||
const setRecorders = useAtomSetter(internalAtoms.localData.recorders);
|
||||
const setTypesTranslations = useAtomSetter(internalAtoms.localData.typesTranslations);
|
||||
|
||||
const { locale } = useRouter();
|
||||
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
||||
|
@ -37,10 +28,6 @@ export const useLocalData = (): void => {
|
|||
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
||||
getFileName("websiteInterfaces")
|
||||
);
|
||||
const { data: rawRecorders } = useFetch<LocalDataGetRecordersQuery>(getFileName("recorders"));
|
||||
const { data: rawTypesTranslations } = useFetch<LocalDataGetRecordersQuery>(
|
||||
getFileName("typesTranslations")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logger.log("Refresh languages");
|
||||
|
@ -61,14 +48,4 @@ export const useLocalData = (): void => {
|
|||
logger.log("Refresh fallback langui");
|
||||
setFallbackLangui(processLangui(rawLangui, "en"));
|
||||
}, [rawLangui, setFallbackLangui]);
|
||||
|
||||
useEffect(() => {
|
||||
logger.log("Refresh recorders");
|
||||
setRecorders(processRecorders(rawRecorders));
|
||||
}, [rawRecorders, setRecorders]);
|
||||
|
||||
useEffect(() => {
|
||||
logger.log("Refresh types translations");
|
||||
setTypesTranslations(processTypesTranslations(rawTypesTranslations));
|
||||
}, [rawTypesTranslations, setTypesTranslations]);
|
||||
};
|
||||
|
|
|
@ -3,10 +3,9 @@ import { useEffect } from "react";
|
|||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
import { getDefaultPreferredLanguages } from "helpers/locales";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
||||
import { userAgent } from "contexts/userAgent";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
export enum ThemeMode {
|
||||
Dark = "dark",
|
||||
|
@ -14,45 +13,13 @@ export enum ThemeMode {
|
|||
Light = "light",
|
||||
}
|
||||
|
||||
export enum PerfMode {
|
||||
On = "on",
|
||||
Auto = "auto",
|
||||
Off = "off",
|
||||
}
|
||||
|
||||
const logger = getLogger("⚙️ [Settings Context]");
|
||||
|
||||
const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", []));
|
||||
const themeModeAtom = atomPairing(atomWithStorage("themeMode", ThemeMode.Auto));
|
||||
const themeModeAtom = atomPairing(atomWithStorage<ThemeMode>("themeMode", ThemeMode.Auto));
|
||||
const darkModeAtom = atomPairing(atom(false));
|
||||
const fontSizeAtom = atomPairing(atomWithStorage("fontSize", 1));
|
||||
const dyslexicAtom = atomPairing(atomWithStorage("isDyslexic", false));
|
||||
const currencyAtom = atomPairing(atomWithStorage("currency", "USD"));
|
||||
const playerNameAtom = atomPairing(atomWithStorage("playerName", ""));
|
||||
const perfModeAtom = atomPairing(atomWithStorage("perfMode", PerfMode.Auto));
|
||||
|
||||
const isPerfModeEnabledAtom = atom((get) => {
|
||||
const os = get(userAgent.os);
|
||||
const engine = get(userAgent.engine);
|
||||
const perfMode = get(perfModeAtom[0]);
|
||||
|
||||
if (os === "iOS") return true;
|
||||
if (engine === "WebKit") return true;
|
||||
if (perfMode === "auto") {
|
||||
if (engine === "Blink") return false;
|
||||
if (os === "Linux") return true;
|
||||
if (os === "Android") return true;
|
||||
}
|
||||
return perfMode === PerfMode.On;
|
||||
});
|
||||
|
||||
const isPerfModeToggleableAtom = atom((get) => {
|
||||
const engine = get(userAgent.engine);
|
||||
const os = get(userAgent.os);
|
||||
if (os === "iOS") return false;
|
||||
if (engine === "WebKit") return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
export const settings = {
|
||||
preferredLanguages: preferredLanguagesAtom,
|
||||
|
@ -62,9 +29,6 @@ export const settings = {
|
|||
dyslexic: dyslexicAtom,
|
||||
currency: currencyAtom,
|
||||
playerName: playerNameAtom,
|
||||
perfMode: perfModeAtom,
|
||||
isPerfModeEnabled: isPerfModeEnabledAtom,
|
||||
isPerfModeToggleable: isPerfModeToggleableAtom,
|
||||
};
|
||||
|
||||
export const useSettings = (): void => {
|
||||
|
@ -116,33 +80,24 @@ export const useSettings = (): void => {
|
|||
}, [isDarkMode]);
|
||||
|
||||
/* PREFERRED LANGUAGES */
|
||||
useEffect(() => {
|
||||
if (!router.locale || !router.locales) return;
|
||||
const localStorageValue: string[] = JSON.parse(
|
||||
localStorage.getItem("preferredLanguages") ?? "[]"
|
||||
);
|
||||
|
||||
if (localStorageValue.length === 0) {
|
||||
const defaultLanguages = router.locales;
|
||||
defaultLanguages.sort((a, b) => {
|
||||
const evaluate = (value: string) =>
|
||||
navigator.languages.includes(value)
|
||||
? navigator.languages.findIndex((v) => value === v)
|
||||
: navigator.languages.length;
|
||||
return evaluate(a) - evaluate(b);
|
||||
});
|
||||
logger.log("First time visitor, initializing preferred languages to", defaultLanguages);
|
||||
setPreferredLanguages(defaultLanguages);
|
||||
} else if (router.locale !== localStorageValue[0]) {
|
||||
logger.log(
|
||||
"Router locale",
|
||||
router.locale,
|
||||
"doesn't correspond to preferred locale. Switching to",
|
||||
localStorageValue[0]
|
||||
useEffect(() => {
|
||||
if (preferredLanguages.length === 0) {
|
||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
||||
}
|
||||
} else if (router.locale !== preferredLanguages[0]) {
|
||||
/*
|
||||
* Using a timeout to the code getting stuck into a loop when reaching the website with a
|
||||
* different preferredLanguages[0] from router.locale
|
||||
*/
|
||||
setTimeout(
|
||||
async () =>
|
||||
router.replace(router.asPath, router.asPath, {
|
||||
locale: preferredLanguages[0],
|
||||
}),
|
||||
250
|
||||
);
|
||||
router.replace(router.asPath, router.asPath, {
|
||||
locale: localStorageValue[0],
|
||||
});
|
||||
}
|
||||
}, [router, setPreferredLanguages, preferredLanguages]);
|
||||
}, [preferredLanguages, router, setPreferredLanguages]);
|
||||
};
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { atom } from "jotai";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
import { useEffect } from "react";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
const logger = getLogger("📱 [User Agent]");
|
||||
|
||||
const osAtom = atomPairing(atom<string | undefined>(undefined));
|
||||
const browserAtom = atomPairing(atom<string | undefined>(undefined));
|
||||
const engineAtom = atomPairing(atom<string | undefined>(undefined));
|
||||
const deviceTypeAtom = atomPairing(atom<string | undefined>(undefined));
|
||||
|
||||
export const userAgent = {
|
||||
os: osAtom[0],
|
||||
browser: browserAtom[0],
|
||||
engine: engineAtom[0],
|
||||
deviceType: deviceTypeAtom[0],
|
||||
};
|
||||
|
||||
export const useUserAgent = (): void => {
|
||||
const setOs = useAtomSetter(osAtom);
|
||||
const setBrowser = useAtomSetter(browserAtom);
|
||||
const setEngine = useAtomSetter(engineAtom);
|
||||
const setDeviceType = useAtomSetter(deviceTypeAtom);
|
||||
|
||||
const isClient = useIsClient();
|
||||
|
||||
useEffect(() => {
|
||||
const parser = new UAParser();
|
||||
|
||||
const os = parser.getOS().name;
|
||||
const browser = parser.getBrowser().name;
|
||||
const engine = parser.getEngine().name;
|
||||
const deviceType = parser.getDevice().type;
|
||||
|
||||
setOs(os);
|
||||
setBrowser(browser);
|
||||
setEngine(engine);
|
||||
setDeviceType(deviceType);
|
||||
|
||||
logger.log({ os, browser, engine, deviceType });
|
||||
}, [isClient, setBrowser, setDeviceType, setEngine, setOs]);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect } from "react";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
import { useIsWebkit } from "hooks/useIsWebkit";
|
||||
|
||||
export const useWebkitFixes = (): void => {
|
||||
const isWebkit = useIsWebkit();
|
||||
|
||||
useEffect(() => {
|
||||
const next = document.getElementById("__next");
|
||||
if (isDefined(next)) {
|
||||
if (isWebkit) {
|
||||
next.classList.add("webkit-fixes");
|
||||
} else {
|
||||
next.classList.remove("webkit-fixes");
|
||||
}
|
||||
}
|
||||
}, [isWebkit]);
|
||||
};
|
|
@ -3,22 +3,8 @@ import { resolve } from "path";
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { config } from "dotenv";
|
||||
import { getReadySdk } from "./sdk";
|
||||
import {
|
||||
LocalDataGetCurrenciesQuery,
|
||||
LocalDataGetLanguagesQuery,
|
||||
LocalDataGetTypesTranslationsQuery,
|
||||
LocalDataGetWebsiteInterfacesQuery,
|
||||
} from "./generated";
|
||||
import {
|
||||
processLangui,
|
||||
Langui,
|
||||
TypesTranslations,
|
||||
processTypesTranslations,
|
||||
Currencies,
|
||||
processCurrencies,
|
||||
Languages,
|
||||
processLanguages,
|
||||
} from "helpers/localData";
|
||||
import { LocalDataGetWebsiteInterfacesQuery } from "./generated";
|
||||
import { processLangui, Langui } from "helpers/localData";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
config({ path: resolve(process.cwd(), ".env.local") });
|
||||
|
@ -26,7 +12,7 @@ config({ path: resolve(process.cwd(), ".env.local") });
|
|||
const LOCAL_DATA_FOLDER = `${process.cwd()}/public/local-data`;
|
||||
const logger = getLogger("💽 [Local Data]", "server");
|
||||
|
||||
const writeLocalData = (name: LocalDataFile, localData: object) => {
|
||||
const writeLocalData = (name: LocalDataFile, localData: unknown) => {
|
||||
const path = `${LOCAL_DATA_FOLDER}/${name}.json`;
|
||||
writeFileSync(path, JSON.stringify(localData), { encoding: "utf-8" });
|
||||
logger.log(`${name}.json has been written`);
|
||||
|
@ -37,68 +23,22 @@ const readLocalData = <T>(name: LocalDataFile): T => {
|
|||
return JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
||||
};
|
||||
|
||||
export const fetchWebsiteInterfaces = async (): Promise<void> => {
|
||||
export const fetchLocalData = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("websiteInterfaces", await sdk.localDataGetWebsiteInterfaces());
|
||||
};
|
||||
|
||||
export const fetchCurrencies = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("currencies", await sdk.localDataGetCurrencies());
|
||||
};
|
||||
|
||||
export const fetchLanguages = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("languages", await sdk.localDataGetLanguages());
|
||||
};
|
||||
|
||||
export const fetchRecorders = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("recorders", await sdk.localDataGetRecorders());
|
||||
};
|
||||
|
||||
export const fetchTypesTranslations = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("typesTranslations", await sdk.localDataGetTypesTranslations());
|
||||
};
|
||||
|
||||
const fetchLocalData = async (): Promise<void> => {
|
||||
await fetchWebsiteInterfaces();
|
||||
await fetchCurrencies();
|
||||
await fetchLanguages();
|
||||
await fetchRecorders();
|
||||
await fetchTypesTranslations();
|
||||
};
|
||||
|
||||
if (process.argv[2] === "--esrun") {
|
||||
fetchLocalData();
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export type LocalDataFile =
|
||||
| "currencies"
|
||||
| "languages"
|
||||
| "recorders"
|
||||
| "typesTranslations"
|
||||
| "websiteInterfaces";
|
||||
export type LocalDataFile = "currencies" | "languages" | "websiteInterfaces";
|
||||
|
||||
export const getLangui = (locale: string): Langui => {
|
||||
export const getLangui = (locale: string | undefined): Langui => {
|
||||
const websiteInterfaces = readLocalData<LocalDataGetWebsiteInterfacesQuery>("websiteInterfaces");
|
||||
return processLangui(websiteInterfaces, locale);
|
||||
};
|
||||
|
||||
export const getTypesTranslations = (): TypesTranslations => {
|
||||
const typesTranslations = readLocalData<LocalDataGetTypesTranslationsQuery>("typesTranslations");
|
||||
return processTypesTranslations(typesTranslations);
|
||||
};
|
||||
|
||||
export const getCurrencies = (): Currencies => {
|
||||
const currencies = readLocalData<LocalDataGetCurrenciesQuery>("currencies");
|
||||
return processCurrencies(currencies);
|
||||
};
|
||||
|
||||
export const getLanguages = (): Languages => {
|
||||
const languages = readLocalData<LocalDataGetLanguagesQuery>("languages");
|
||||
return processLanguages(languages);
|
||||
};
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
fragment parentFolderPreview on ContentsFolder {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
fragment recorderChip on Recorder {
|
||||
username
|
||||
anonymize
|
||||
anonymous_code
|
||||
pronouns
|
||||
bio(filters: { language: { code: { eq: $language_code } } }) {
|
||||
bio
|
||||
}
|
||||
languages(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
avatar {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,8 +14,9 @@ fragment relatedContentPreview on Content {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +24,9 @@ fragment relatedContentPreview on Content {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ import { GetStaticProps } from "next";
|
|||
import { getReadySdk } from "./sdk";
|
||||
import { PostWithTranslations } from "types/types";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { prettyDate, prettySlug } from "helpers/formatters";
|
||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import { getDescription } from "helpers/description";
|
||||
import { AppLayoutRequired } from "components/AppLayout";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
|
@ -17,40 +17,45 @@ export const getPostStaticProps =
|
|||
(slug: string): GetStaticProps =>
|
||||
async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format, formatCategory, formatDate } = getFormat(context.locale);
|
||||
const { format } = getFormat(context.locale);
|
||||
const post = await sdk.getPost({
|
||||
slug: slug,
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (
|
||||
post.posts?.data &&
|
||||
post.posts.data.length > 0 &&
|
||||
post.posts.data[0]?.attributes?.translations &&
|
||||
isDefined(context.locale) &&
|
||||
isDefined(context.locales)
|
||||
) {
|
||||
const selectedTranslation = staticSmartLanguage({
|
||||
items: post.posts.data[0].attributes.translations,
|
||||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
||||
});
|
||||
|
||||
if (!post.posts?.data[0]?.attributes?.translations || !context.locale || !context.locales) {
|
||||
return { notFound: true };
|
||||
const title = selectedTranslation?.title ?? prettySlug(slug);
|
||||
|
||||
const description = getDescription(selectedTranslation?.excerpt, {
|
||||
[format("release_date")]: [prettyDate(post.posts.data[0].attributes.date, context.locale)],
|
||||
[format("category", { count: Infinity })]: filterHasAttributes(
|
||||
post.posts.data[0].attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => category.attributes.short),
|
||||
});
|
||||
|
||||
const thumbnail =
|
||||
selectedTranslation?.thumbnail?.data?.attributes ??
|
||||
post.posts.data[0].attributes.thumbnail?.data?.attributes;
|
||||
|
||||
const props: PostStaticProps = {
|
||||
post: post.posts.data[0].attributes as PostWithTranslations,
|
||||
openGraph: getOpenGraph(format, title, description, thumbnail),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedTranslation = staticSmartLanguage({
|
||||
items: post.posts.data[0].attributes.translations,
|
||||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
||||
});
|
||||
|
||||
const title = selectedTranslation?.title ?? prettySlug(slug);
|
||||
|
||||
const description = getDescription(selectedTranslation?.excerpt ?? selectedTranslation?.body, {
|
||||
[format("release_date")]: [formatDate(post.posts.data[0].attributes.date)],
|
||||
[format("category", { count: Infinity })]: filterHasAttributes(
|
||||
post.posts.data[0].attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => formatCategory(category.attributes.slug)),
|
||||
});
|
||||
|
||||
const thumbnail =
|
||||
selectedTranslation?.thumbnail?.data?.attributes ??
|
||||
post.posts.data[0].attributes.thumbnail?.data?.attributes;
|
||||
|
||||
const props: PostStaticProps = {
|
||||
post: post.posts.data[0].attributes as PostWithTranslations,
|
||||
openGraph: getOpenGraph(format, title, description, thumbnail),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
return { notFound: true };
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface ICUParams {
|
|||
category: { count: number };
|
||||
size: never;
|
||||
release_date: never;
|
||||
release_year: never;
|
||||
details: never;
|
||||
price: never;
|
||||
width: never;
|
||||
|
@ -40,9 +41,13 @@ export interface ICUParams {
|
|||
front_matter: never;
|
||||
back_matter: never;
|
||||
open_content: never;
|
||||
read_content: never;
|
||||
watch_content: never;
|
||||
listen_content: never;
|
||||
view_scans: never;
|
||||
paperback: never;
|
||||
hardcover: never;
|
||||
select_language: never;
|
||||
language: { count: number };
|
||||
library_description: never;
|
||||
wiki_description: never;
|
||||
|
@ -57,6 +62,7 @@ export interface ICUParams {
|
|||
show_primary_items: never;
|
||||
show_secondary_items: never;
|
||||
order_by: never;
|
||||
group_by: never;
|
||||
select_option_sidebar: never;
|
||||
group: never;
|
||||
settings: never;
|
||||
|
@ -78,12 +84,17 @@ export interface ICUParams {
|
|||
review: never;
|
||||
done: never;
|
||||
status: never;
|
||||
transcribers: never;
|
||||
translators: never;
|
||||
proofreaders: never;
|
||||
transcript_notice: never;
|
||||
translation_notice: never;
|
||||
source_language: never;
|
||||
pronouns: never;
|
||||
item: { count: number };
|
||||
content: never;
|
||||
open_settings: never;
|
||||
change_language: never;
|
||||
open_search: never;
|
||||
chronology: never;
|
||||
accords_handbook: never;
|
||||
|
@ -101,14 +112,16 @@ export interface ICUParams {
|
|||
item_not_available: never;
|
||||
primary_language: never;
|
||||
secondary_language: never;
|
||||
combine_related_contents: never;
|
||||
previous_content: { count: number };
|
||||
followup_content: { count: number };
|
||||
videos: never;
|
||||
view_on_x: { x: Date | boolean | number | string };
|
||||
view_on: never;
|
||||
channel: never;
|
||||
subscribers: never;
|
||||
description: never;
|
||||
available_at_x: { x: Date | boolean | number | string };
|
||||
available_at: never;
|
||||
search_title: never;
|
||||
want_it: never;
|
||||
have_it: never;
|
||||
source: never;
|
||||
|
@ -116,6 +129,7 @@ export interface ICUParams {
|
|||
only_display_items_i_have: never;
|
||||
only_display_items_i_want: never;
|
||||
only_display_unmarked_items: never;
|
||||
display_all_items: never;
|
||||
table_of_contents: never;
|
||||
no_results_message: never;
|
||||
all: never;
|
||||
|
@ -126,6 +140,7 @@ export interface ICUParams {
|
|||
cleaners: never;
|
||||
typesetters: never;
|
||||
notes: never;
|
||||
cover: never;
|
||||
tags: never;
|
||||
no_source_warning: never;
|
||||
copy_anchor_link: never;
|
||||
|
@ -134,6 +149,7 @@ export interface ICUParams {
|
|||
empty_folder_message: never;
|
||||
switch_to_grid_view: never;
|
||||
switch_to_folder_view: never;
|
||||
content_is_not_available: never;
|
||||
paper_texture: never;
|
||||
book_fold: never;
|
||||
lighting: never;
|
||||
|
@ -161,20 +177,11 @@ export interface ICUParams {
|
|||
x_results: { x: number };
|
||||
definition_x: { x: Date | boolean | number | string };
|
||||
subitem_of_x: { x: Date | boolean | number | string };
|
||||
variant_of_x: { x: Date | boolean | number | string };
|
||||
dark_mode_extension_warning: never;
|
||||
weapon: { count: number };
|
||||
weapons_description: never;
|
||||
level_x: { x: Date | boolean | number | string };
|
||||
story_x: { x: Date | boolean | number | string };
|
||||
player_name_tooltip: never;
|
||||
download_archive: never;
|
||||
search_placeholder: never;
|
||||
performance_mode: never;
|
||||
performance_mode_tooltip: never;
|
||||
transcriber: { count: number };
|
||||
translator: { count: number };
|
||||
proofreader: { count: number };
|
||||
dubber: { count: number };
|
||||
subber: { count: number };
|
||||
author: { count: number };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getChronicle($slug: String) {
|
||||
query getChronicle($slug: String, $language_code: String) {
|
||||
chronicles(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
attributes {
|
||||
|
@ -53,21 +53,21 @@ query getChronicle($slug: String) {
|
|||
authors {
|
||||
data {
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
translators {
|
||||
data {
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
proofreaders {
|
||||
data {
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,8 +80,10 @@ query getChronicle($slug: String) {
|
|||
slug
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +91,9 @@ query getChronicle($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +123,7 @@ query getChronicle($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +131,7 @@ query getChronicle($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +139,7 @@ query getChronicle($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getContentText($slug: String) {
|
||||
query getContentText($slug: String, $language_code: String) {
|
||||
contents(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -6,8 +6,10 @@ query getContentText($slug: String) {
|
|||
slug
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +17,9 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,8 +53,10 @@ query getContentText($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,15 +67,19 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platform {
|
||||
platforms(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +89,9 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +101,9 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +113,9 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +123,9 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +163,7 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +171,7 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,44 +179,7 @@ query getContentText($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
notes
|
||||
}
|
||||
video_set {
|
||||
status
|
||||
source_language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
has_subfile
|
||||
subbers(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
notes
|
||||
}
|
||||
audio_set {
|
||||
status
|
||||
source_language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
dubbers(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,37 @@
|
|||
query getContentsFolder($slug: String) {
|
||||
query getContentsFolder($slug: String, $language_code: String) {
|
||||
contentsFolders(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
|
@ -22,8 +51,10 @@ query getContentsFolder($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +62,9 @@ query getContentsFolder($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,55 +124,6 @@ query getContentsFolder($slug: String) {
|
|||
}
|
||||
}
|
||||
}
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
...parentFolderPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getLibraryItem($slug: String) {
|
||||
query getLibraryItem($slug: String, $language_code: String) {
|
||||
libraryItems(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -9,7 +9,6 @@ query getLibraryItem($slug: String) {
|
|||
root_item
|
||||
primary
|
||||
digital
|
||||
download_available
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
|
@ -33,8 +32,10 @@ query getLibraryItem($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +57,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,15 +80,19 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platform {
|
||||
platforms(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,20 +126,21 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tracks(pagination: { limit: -1 }) {
|
||||
id
|
||||
slug
|
||||
title
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGroup {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +148,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -176,8 +188,10 @@ query getLibraryItem($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,16 +202,19 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platform {
|
||||
platforms {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,6 +224,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,6 +236,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,6 +248,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,6 +258,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -282,8 +311,10 @@ query getLibraryItem($slug: String) {
|
|||
slug
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,6 +322,9 @@ query getLibraryItem($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getLibraryItemScans($slug: String) {
|
||||
query getLibraryItemScans($slug: String, $language_code: String) {
|
||||
libraryItems(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -6,7 +6,6 @@ query getLibraryItemScans($slug: String) {
|
|||
slug
|
||||
title
|
||||
subtitle
|
||||
download_available
|
||||
images(pagination: { limit: -1 }) {
|
||||
status
|
||||
language {
|
||||
|
@ -27,7 +26,7 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +34,7 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +42,7 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,8 +155,10 @@ query getLibraryItemScans($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -169,16 +170,19 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platform {
|
||||
platforms {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,6 +192,9 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,6 +204,9 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +216,9 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -213,6 +226,9 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,7 +289,7 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +297,7 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +305,7 @@ query getLibraryItemScans($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getPost($slug: String) {
|
||||
query getPost($slug: String, $language_code: String) {
|
||||
posts(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -12,14 +12,16 @@ query getPost($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,9 @@ query getVideo($uid: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getWeapon($slug: String) {
|
||||
query getWeapon($slug: String, $language_code: String) {
|
||||
weaponStories(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
attributes {
|
||||
|
@ -7,8 +7,10 @@ query getWeapon($slug: String) {
|
|||
id
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +57,16 @@ fragment sharedWeaponFragment on WeaponStory {
|
|||
id
|
||||
attributes {
|
||||
slug
|
||||
translations(filters: { language: { code: { eq: $language_code } } }) {
|
||||
name
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getWikiPage($slug: String) {
|
||||
query getWikiPage($slug: String, $language_code: String) {
|
||||
wikiPages(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -13,15 +13,21 @@ query getWikiPage($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
tags {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +58,7 @@ query getWikiPage($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +66,7 @@ query getWikiPage($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +74,7 @@ query getWikiPage($slug: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
username
|
||||
...recorderChip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,8 +90,10 @@ query getWikiPage($slug: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
query localDataGetRecorders {
|
||||
recorders(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
username
|
||||
anonymize
|
||||
anonymous_code
|
||||
pronouns
|
||||
bio(pagination: { limit: -1 }) {
|
||||
bio
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
languages(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
avatar {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
query localDataGetTypesTranslations {
|
||||
metadataTypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioSubtypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoSubtypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textualSubtypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupSubtypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gamePlatforms(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentTypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wikiPagesTags(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
weaponStoryTypes(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
translations {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ query localDataGetWebsiteInterfaces {
|
|||
category
|
||||
size
|
||||
release_date
|
||||
release_year
|
||||
details
|
||||
price
|
||||
width
|
||||
|
@ -50,9 +51,13 @@ query localDataGetWebsiteInterfaces {
|
|||
front_matter
|
||||
back_matter
|
||||
open_content
|
||||
read_content
|
||||
watch_content
|
||||
listen_content
|
||||
view_scans
|
||||
paperback
|
||||
hardcover
|
||||
select_language
|
||||
language
|
||||
library_description
|
||||
wiki_description
|
||||
|
@ -67,6 +72,7 @@ query localDataGetWebsiteInterfaces {
|
|||
show_primary_items
|
||||
show_secondary_items
|
||||
order_by
|
||||
group_by
|
||||
select_option_sidebar
|
||||
group
|
||||
settings
|
||||
|
@ -88,12 +94,17 @@ query localDataGetWebsiteInterfaces {
|
|||
review
|
||||
done
|
||||
status
|
||||
transcribers
|
||||
translators
|
||||
proofreaders
|
||||
transcript_notice
|
||||
translation_notice
|
||||
source_language
|
||||
pronouns
|
||||
item
|
||||
content
|
||||
open_settings
|
||||
change_language
|
||||
open_search
|
||||
chronology
|
||||
accords_handbook
|
||||
|
@ -111,14 +122,16 @@ query localDataGetWebsiteInterfaces {
|
|||
item_not_available
|
||||
primary_language
|
||||
secondary_language
|
||||
combine_related_contents
|
||||
previous_content
|
||||
followup_content
|
||||
videos
|
||||
view_on_x
|
||||
view_on
|
||||
channel
|
||||
subscribers
|
||||
description
|
||||
available_at_x
|
||||
available_at
|
||||
search_title
|
||||
want_it
|
||||
have_it
|
||||
source
|
||||
|
@ -126,6 +139,7 @@ query localDataGetWebsiteInterfaces {
|
|||
only_display_items_i_have
|
||||
only_display_items_i_want
|
||||
only_display_unmarked_items
|
||||
display_all_items
|
||||
table_of_contents
|
||||
no_results_message
|
||||
all
|
||||
|
@ -136,6 +150,7 @@ query localDataGetWebsiteInterfaces {
|
|||
cleaners
|
||||
typesetters
|
||||
notes
|
||||
cover
|
||||
tags
|
||||
no_source_warning
|
||||
copy_anchor_link
|
||||
|
@ -144,6 +159,7 @@ query localDataGetWebsiteInterfaces {
|
|||
empty_folder_message
|
||||
switch_to_grid_view
|
||||
switch_to_folder_view
|
||||
content_is_not_available
|
||||
paper_texture
|
||||
book_fold
|
||||
lighting
|
||||
|
@ -168,22 +184,13 @@ query localDataGetWebsiteInterfaces {
|
|||
x_results
|
||||
definition_x
|
||||
subitem_of_x
|
||||
variant_of_x
|
||||
dark_mode_extension_warning
|
||||
weapon
|
||||
weapons_description
|
||||
level_x
|
||||
story_x
|
||||
player_name_tooltip
|
||||
download_archive
|
||||
search_placeholder
|
||||
performance_mode
|
||||
performance_mode_tooltip
|
||||
transcriber
|
||||
translator
|
||||
proofreader
|
||||
dubber
|
||||
subber
|
||||
author
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
/* eslint-disable import/no-nodejs-modules */
|
||||
import { exit } from "process";
|
||||
import { execSync } from "child_process";
|
||||
import chalk from "chalk";
|
||||
import { getLangui } from "./fetchLocalData";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
const logger = getLogger("💽 [Unused wording keys]", "server");
|
||||
|
||||
const unusedWordingKeys = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { ui_language, ...langui } = getLangui("en");
|
||||
const results = Object.keys(langui).map((oKey) => {
|
||||
const buffer = execSync(`grep -r "format(\\"${oKey}\\"" -o src | wc -l`).toString();
|
||||
const result = parseInt(buffer.trim(), 10);
|
||||
if (result === 0) {
|
||||
logger.error(`"${oKey}" was not found in the codebase`);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const foundUnusedCount = results.filter((result) => result === 0).length;
|
||||
|
||||
if (foundUnusedCount > 0) {
|
||||
console.log();
|
||||
console.error(chalk.red(`${chalk.bold(foundUnusedCount)} wording keys are unused`));
|
||||
exit(1);
|
||||
} else {
|
||||
console.log(`${chalk.bold(foundUnusedCount)} wording key are unused`);
|
||||
exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
if (process.argv[2] === "--uwk") {
|
||||
unusedWordingKeys();
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import { ReactNode, useMemo } from "react";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { insertInBetweenArray } from "helpers/others";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
|
@ -45,19 +44,3 @@ export const ElementsSeparator = ({
|
|||
}: ElementsSeparatorProps): JSX.Element => (
|
||||
<>{insertInBetweenArray(children.filter(Boolean), separator)}</>
|
||||
);
|
||||
|
||||
interface FormatWithComponentProps {
|
||||
text: string;
|
||||
component: React.ReactNode;
|
||||
}
|
||||
|
||||
export const formatWithComponentSplitter = " [SPLITTER] ";
|
||||
export const FormatWithComponent = ({ text, component }: FormatWithComponentProps): JSX.Element => {
|
||||
const splittedText = useMemo<ReactNode[]>(() => {
|
||||
const result = text.split("[SPLITTER]");
|
||||
return result;
|
||||
}, [text]);
|
||||
|
||||
console.log(splittedText);
|
||||
return <ElementsSeparator separator={component}>{splittedText}</ElementsSeparator>;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { prettyMarkdown } from "helpers/formatters";
|
||||
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "./asserts";
|
||||
|
||||
export const getDescription = (
|
||||
description: string | null | undefined,
|
||||
|
@ -7,6 +6,13 @@ export const getDescription = (
|
|||
): string => {
|
||||
let result = "";
|
||||
|
||||
if (isDefinedAndNotEmpty(description)) {
|
||||
result += prettyMarkdown(description);
|
||||
if (isDefined(chipsGroups)) {
|
||||
result += "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in chipsGroups) {
|
||||
if (Object.hasOwn(chipsGroups, key)) {
|
||||
const chipsGroup = filterDefined(chipsGroups[key]);
|
||||
|
@ -16,16 +22,12 @@ export const getDescription = (
|
|||
}
|
||||
}
|
||||
|
||||
if (isDefinedAndNotEmpty(description)) {
|
||||
if (result !== "") {
|
||||
result += "\n";
|
||||
}
|
||||
result += prettyMarkdown(description);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const prettyMarkdown = (markdown: string): string =>
|
||||
markdown.replace(/[*]/gu, "").replace(/[_]/gu, "");
|
||||
|
||||
const prettyChip = (items: (string | undefined)[]): string =>
|
||||
items
|
||||
.filter((item) => isDefined(item))
|
||||
|
|
|
@ -1,7 +1,39 @@
|
|||
import { convert } from "html-to-text";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { Renderer, marked } from "marked";
|
||||
import { isDefinedAndNotEmpty } from "./asserts";
|
||||
import { convertPrice } from "./numbers";
|
||||
import { isDefinedAndNotEmpty, isUndefined } from "./asserts";
|
||||
import { datePickerToDate } from "./date";
|
||||
import { Currencies, Languages } from "./localData";
|
||||
import { DatePickerFragment, PricePickerFragment } from "graphql/generated";
|
||||
|
||||
export const prettyDate = (
|
||||
datePicker: DatePickerFragment,
|
||||
locale = "en",
|
||||
dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium"
|
||||
): string => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle });
|
||||
|
||||
export const prettyPrice = (
|
||||
pricePicker: PricePickerFragment,
|
||||
currencies: Currencies,
|
||||
targetCurrencyCode?: string
|
||||
): string => {
|
||||
if (!targetCurrencyCode) return "";
|
||||
if (isUndefined(pricePicker.amount)) return "";
|
||||
|
||||
const targetCurrency = currencies.find(
|
||||
(currency) => currency.attributes?.code === targetCurrencyCode
|
||||
);
|
||||
|
||||
if (targetCurrency?.attributes) {
|
||||
const amountInTargetCurrency = convertPrice(pricePicker, targetCurrency);
|
||||
return amountInTargetCurrency.toLocaleString("en", {
|
||||
style: "currency",
|
||||
currency: targetCurrency.attributes.code,
|
||||
});
|
||||
}
|
||||
return pricePicker.amount.toLocaleString("en", {
|
||||
style: "currency",
|
||||
currency: pricePicker.currency?.data?.attributes?.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const prettySlug = (slug?: string, parentSlug?: string): string => {
|
||||
let newSlug = slug;
|
||||
|
@ -26,6 +58,135 @@ export const prettyInlineTitle = (
|
|||
return result;
|
||||
};
|
||||
|
||||
export const prettyItemSubType = (
|
||||
metadata:
|
||||
| {
|
||||
__typename: "ComponentMetadataAudio";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataBooks";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataGame";
|
||||
platforms?: {
|
||||
data: {
|
||||
id?: string | null;
|
||||
attributes?: {
|
||||
short: string;
|
||||
} | null;
|
||||
}[];
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataGroup";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
subitems_type?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataVideo";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| { __typename: "ComponentMetadataOther" }
|
||||
| { __typename: "Error" }
|
||||
| null
|
||||
): string => {
|
||||
if (metadata) {
|
||||
switch (metadata.__typename) {
|
||||
case "ComponentMetadataAudio":
|
||||
case "ComponentMetadataBooks":
|
||||
case "ComponentMetadataVideo":
|
||||
return metadata.subtype?.data?.attributes?.titles &&
|
||||
metadata.subtype.data.attributes.titles.length > 0 &&
|
||||
metadata.subtype.data.attributes.titles[0]
|
||||
? metadata.subtype.data.attributes.titles[0].title
|
||||
: prettySlug(metadata.subtype?.data?.attributes?.slug);
|
||||
case "ComponentMetadataGame":
|
||||
return metadata.platforms?.data &&
|
||||
metadata.platforms.data.length > 0 &&
|
||||
metadata.platforms.data[0]?.attributes
|
||||
? metadata.platforms.data[0].attributes.short
|
||||
: "";
|
||||
case "ComponentMetadataGroup": {
|
||||
const firstPart =
|
||||
metadata.subtype?.data?.attributes?.titles &&
|
||||
metadata.subtype.data.attributes.titles.length > 0 &&
|
||||
metadata.subtype.data.attributes.titles[0]
|
||||
? metadata.subtype.data.attributes.titles[0].title
|
||||
: prettySlug(metadata.subtype?.data?.attributes?.slug);
|
||||
|
||||
const secondPart =
|
||||
metadata.subitems_type?.data?.attributes?.titles &&
|
||||
metadata.subitems_type.data.attributes.titles.length > 0 &&
|
||||
metadata.subitems_type.data.attributes.titles[0]
|
||||
? metadata.subitems_type.data.attributes.titles[0].title
|
||||
: prettySlug(metadata.subitems_type?.data?.attributes?.slug);
|
||||
return `${secondPart} ${firstPart}`;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
/* eslint-enable id-denylist */
|
||||
|
||||
export const prettyShortenNumber = (number: number): string => {
|
||||
if (number > 1_000_000) {
|
||||
return `${(number / 1_000_000).toLocaleString(undefined, {
|
||||
|
@ -54,7 +215,15 @@ export const prettyDuration = (seconds: number): string => {
|
|||
let result = "";
|
||||
if (hours) result += `${hours.toString().padStart(2, "0")}:`;
|
||||
result += `${minutes.toString().padStart(2, "0")}:`;
|
||||
result += Math.floor(remainingSeconds).toString().padStart(2, "0");
|
||||
result += remainingSeconds.toString().padStart(2, "0");
|
||||
return result;
|
||||
};
|
||||
|
||||
export const prettyLanguage = (code: string, languages: Languages): string => {
|
||||
let result = code;
|
||||
languages.forEach((language) => {
|
||||
if (language.attributes?.code === code) result = language.attributes.localized_name;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
|
@ -92,41 +261,3 @@ export const slugify = (string: string | undefined): string => {
|
|||
};
|
||||
|
||||
export const sJoin = (...args: (string | null | undefined)[]): string => args.join("");
|
||||
|
||||
export const prettyMarkdown = (markdown: string): string => {
|
||||
const block = (text: string) => `${text}\n\n`;
|
||||
const escapeBlock = (text: string) => `${escape(text)}\n\n`;
|
||||
const line = (text: string) => `${text}\n`;
|
||||
const inline = (text: string) => text;
|
||||
const newline = () => "\n";
|
||||
const empty = () => "";
|
||||
|
||||
const TxtRenderer: Renderer = {
|
||||
// Block elements
|
||||
code: escapeBlock,
|
||||
blockquote: block,
|
||||
html: empty,
|
||||
heading: block,
|
||||
hr: newline,
|
||||
list: (text) => block(text.trim()),
|
||||
listitem: line,
|
||||
checkbox: empty,
|
||||
paragraph: block,
|
||||
table: (header, body) => line(header + body),
|
||||
tablerow: (text) => line(text.trim()),
|
||||
tablecell: (text) => `${text} `,
|
||||
// Inline elements
|
||||
strong: inline,
|
||||
em: inline,
|
||||
codespan: inline,
|
||||
br: newline,
|
||||
del: inline,
|
||||
link: (_0, _1, text) => text,
|
||||
image: (_0, _1, text) => text,
|
||||
text: inline,
|
||||
// etc.
|
||||
options: {},
|
||||
};
|
||||
|
||||
return convert(sanitize(marked(markdown, { renderer: TxtRenderer }))).trim();
|
||||
};
|
||||
|
|
|
@ -1,21 +1,8 @@
|
|||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
import {
|
||||
DatePickerFragment,
|
||||
LibraryItemMetadataDynamicZone,
|
||||
PricePickerFragment,
|
||||
} from "graphql/generated";
|
||||
import { LibraryItemMetadataDynamicZone } from "graphql/generated";
|
||||
import { ICUParams } from "graphql/icuParams";
|
||||
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
|
||||
import {
|
||||
getCurrencies,
|
||||
getLanguages,
|
||||
getLangui,
|
||||
getTypesTranslations,
|
||||
} from "graphql/fetchLocalData";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { LibraryItemMetadata } from "types/types";
|
||||
import { datePickerToDate } from "helpers/date";
|
||||
import { convertPrice } from "helpers/numbers";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { getLangui } from "graphql/fetchLocalData";
|
||||
|
||||
type WordingKey = keyof ICUParams;
|
||||
type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>;
|
||||
|
@ -42,32 +29,18 @@ const componentSetsTextsetStatusToWording: Record<
|
|||
};
|
||||
|
||||
export const getFormat = (
|
||||
locale: string | undefined = "en"
|
||||
locale: string | undefined
|
||||
): {
|
||||
format: <K extends WordingKey>(
|
||||
key: K,
|
||||
...values: ICUParams[K] extends never ? [undefined?] : [ICUParams[K]]
|
||||
) => string;
|
||||
formatLibraryItemType: (metadata: LibraryItemMetadata) => string;
|
||||
formatLibraryItemSubType: (metadata: LibraryItemMetadata) => string;
|
||||
formatLibraryItemType: (metadata: { __typename: LibraryItemType }) => string;
|
||||
formatStatusLabel: (status: ContentStatus) => string;
|
||||
formatStatusDescription: (status: ContentStatus) => string;
|
||||
formatCategory: (slug: string, type?: "default" | "full") => string;
|
||||
formatContentType: (slug: string) => string;
|
||||
formatWikiTag: (slug: string) => string;
|
||||
formatWeaponType: (slug: string) => string;
|
||||
formatLanguage: (code: string) => string;
|
||||
formatPrice: (price: PricePickerFragment, targetCurrencyCode?: string) => string;
|
||||
formatDate: (
|
||||
datePicker: DatePickerFragment,
|
||||
dateStyle?: Intl.DateTimeFormatOptions["dateStyle"]
|
||||
) => string;
|
||||
} => {
|
||||
const langui = getLangui(locale);
|
||||
const fallbackLangui = getLangui("en");
|
||||
const typesTranslations = getTypesTranslations();
|
||||
const currencies = getCurrencies();
|
||||
const languages = getLanguages();
|
||||
|
||||
const format = (
|
||||
key: WordingKey,
|
||||
|
@ -92,90 +65,8 @@ export const getFormat = (
|
|||
return key;
|
||||
};
|
||||
|
||||
const formatLibraryItemType = (metadata: LibraryItemMetadata): string =>
|
||||
metadata ? format(componentMetadataToWording[metadata.__typename]) : format("other");
|
||||
|
||||
const formatLibraryItemSubType = (metadata: LibraryItemMetadata): string => {
|
||||
switch (metadata?.__typename) {
|
||||
case "ComponentMetadataAudio": {
|
||||
const slug = metadata.subtype?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.audioSubtypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataBooks": {
|
||||
const slug = metadata.subtype?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.textualSubtypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataVideo": {
|
||||
const slug = metadata.subtype?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.videoSubtypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataGame": {
|
||||
const slug = metadata.platform?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.gamePlatforms.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
console.log(slug);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataGroup": {
|
||||
const subItemType = (() => {
|
||||
const subitemTypeSlug = metadata.subitems_type?.data?.attributes?.slug;
|
||||
const subItemTypeTranslations = typesTranslations.metadataTypes.find(
|
||||
(type) => type.attributes?.slug === subitemTypeSlug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subItemTypeTranslations?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
})();
|
||||
const groupType = (() => {
|
||||
const groupTypeSlug = metadata.subtype?.data?.attributes?.slug;
|
||||
const groupTypeTranslations = typesTranslations.groupSubtypes.find(
|
||||
(type) => type.attributes?.slug === groupTypeSlug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
groupTypeTranslations?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
})();
|
||||
return `${groupType} - ${subItemType}`;
|
||||
}
|
||||
|
||||
default:
|
||||
return format("other");
|
||||
}
|
||||
};
|
||||
const formatLibraryItemType = (metadata: { __typename: LibraryItemType }): string =>
|
||||
format(componentMetadataToWording[metadata.__typename]);
|
||||
|
||||
const formatStatusLabel = (status: ContentStatus): string =>
|
||||
format(componentSetsTextsetStatusToWording[status].label);
|
||||
|
@ -183,97 +74,10 @@ export const getFormat = (
|
|||
const formatStatusDescription = (status: ContentStatus): string =>
|
||||
format(componentSetsTextsetStatusToWording[status].description);
|
||||
|
||||
const formatCategory = (slug: string, type: "default" | "full" = "default"): string => {
|
||||
const category = typesTranslations.categories.find((cat) => cat.attributes?.slug === slug);
|
||||
if (!category) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = category.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return type === "default" ? localeTranslation?.title : localeTranslation?.short;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
};
|
||||
|
||||
const formatContentType = (slug: string): string => {
|
||||
const contentType = typesTranslations.contentTypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
if (!contentType) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = contentType.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return localeTranslation?.title;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
};
|
||||
|
||||
const formatWikiTag = (slug: string): string => {
|
||||
const wikiTag = typesTranslations.wikiPagesTags.find((cat) => cat.attributes?.slug === slug);
|
||||
if (!wikiTag) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = wikiTag.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return localeTranslation?.title;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
};
|
||||
|
||||
const formatWeaponType = (slug: string): string => {
|
||||
const weaponType = typesTranslations.weaponTypes.find((type) => type.attributes?.slug === slug);
|
||||
if (!weaponType) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = weaponType.attributes?.translations?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return localeTranslation?.name;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
};
|
||||
|
||||
const formatLanguage = (code: string) =>
|
||||
languages.find((language) => language.attributes?.code === code)?.attributes?.localized_name ??
|
||||
code.toUpperCase();
|
||||
|
||||
const formatPrice = (price: PricePickerFragment, targetCurrencyCode?: string) => {
|
||||
if (isUndefined(price.amount)) return "";
|
||||
|
||||
const targetCurrency = currencies.find(
|
||||
(currency) => currency.attributes?.code === targetCurrencyCode
|
||||
);
|
||||
|
||||
if (targetCurrency?.attributes) {
|
||||
const amountInTargetCurrency = convertPrice(price, targetCurrency);
|
||||
return amountInTargetCurrency.toLocaleString(locale, {
|
||||
style: "currency",
|
||||
currency: targetCurrency.attributes.code,
|
||||
});
|
||||
}
|
||||
return price.amount.toLocaleString(locale, {
|
||||
style: "currency",
|
||||
currency: price.currency?.data?.attributes?.code,
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (
|
||||
datePicker: DatePickerFragment,
|
||||
dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium"
|
||||
): string => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle });
|
||||
|
||||
return {
|
||||
format,
|
||||
formatLibraryItemType,
|
||||
formatLibraryItemSubType,
|
||||
formatStatusLabel,
|
||||
formatStatusDescription,
|
||||
formatCategory,
|
||||
formatContentType,
|
||||
formatWikiTag,
|
||||
formatWeaponType,
|
||||
formatLanguage,
|
||||
formatPrice,
|
||||
formatDate,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,9 +13,3 @@ export const isUntangibleGroupItem = (
|
|||
metadata.__typename === "ComponentMetadataGroup" &&
|
||||
(metadata.subtype?.data?.attributes?.slug === "variant-set" ||
|
||||
metadata.subtype?.data?.attributes?.slug === "relation-set");
|
||||
|
||||
export const getScanArchiveURL = (slug: string): string =>
|
||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/library/scans/${slug}.zip`;
|
||||
|
||||
export const getTrackURL = (itemSlug: string, trackSlug: string): string =>
|
||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/library/tracks/${itemSlug}/${trackSlug}.mp3`;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
LocalDataGetCurrenciesQuery,
|
||||
LocalDataGetLanguagesQuery,
|
||||
LocalDataGetRecordersQuery,
|
||||
LocalDataGetTypesTranslationsQuery,
|
||||
LocalDataGetWebsiteInterfacesQuery,
|
||||
} from "graphql/generated";
|
||||
|
||||
|
@ -47,58 +45,3 @@ export const processLanguages = (languages: LocalDataGetLanguagesQuery | undefin
|
|||
}
|
||||
return languages?.languages?.data ?? [];
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export type Recorders = NonNullable<LocalDataGetRecordersQuery["recorders"]>["data"];
|
||||
|
||||
export const processRecorders = (recorders: LocalDataGetRecordersQuery | undefined): Recorders =>
|
||||
recorders?.recorders?.data ?? [];
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export type TypesTranslations = {
|
||||
audioSubtypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["audioSubtypes"]
|
||||
>["data"];
|
||||
gamePlatforms: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["gamePlatforms"]
|
||||
>["data"];
|
||||
groupSubtypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["groupSubtypes"]
|
||||
>["data"];
|
||||
metadataTypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["metadataTypes"]
|
||||
>["data"];
|
||||
textualSubtypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["textualSubtypes"]
|
||||
>["data"];
|
||||
videoSubtypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["videoSubtypes"]
|
||||
>["data"];
|
||||
contentTypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["contentTypes"]
|
||||
>["data"];
|
||||
wikiPagesTags: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["wikiPagesTags"]
|
||||
>["data"];
|
||||
weaponTypes: NonNullable<
|
||||
NonNullable<LocalDataGetTypesTranslationsQuery>["weaponStoryTypes"]
|
||||
>["data"];
|
||||
categories: NonNullable<NonNullable<LocalDataGetTypesTranslationsQuery>["categories"]>["data"];
|
||||
};
|
||||
|
||||
export const processTypesTranslations = (
|
||||
data: LocalDataGetTypesTranslationsQuery | undefined
|
||||
): TypesTranslations => ({
|
||||
audioSubtypes: data?.audioSubtypes?.data ?? [],
|
||||
categories: data?.categories?.data ?? [],
|
||||
contentTypes: data?.contentTypes?.data ?? [],
|
||||
gamePlatforms: data?.gamePlatforms?.data ?? [],
|
||||
groupSubtypes: data?.groupSubtypes?.data ?? [],
|
||||
metadataTypes: data?.metadataTypes?.data ?? [],
|
||||
textualSubtypes: data?.textualSubtypes?.data ?? [],
|
||||
videoSubtypes: data?.videoSubtypes?.data ?? [],
|
||||
weaponTypes: data?.weaponStoryTypes?.data ?? [],
|
||||
wikiPagesTags: data?.wikiPagesTags?.data ?? [],
|
||||
});
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { isDefined } from "./asserts";
|
||||
|
||||
export const getDefaultPreferredLanguages = (routerLocal: string, locales: string[]): string[] => {
|
||||
const defaultPreferredLanguages: Set<string> = new Set();
|
||||
defaultPreferredLanguages.add(routerLocal);
|
||||
defaultPreferredLanguages.add("en");
|
||||
locales.forEach((locale) => defaultPreferredLanguages.add(locale));
|
||||
return [...defaultPreferredLanguages.values()];
|
||||
let defaultPreferredLanguages: string[] = [];
|
||||
if (routerLocal === "en") {
|
||||
defaultPreferredLanguages = [routerLocal];
|
||||
locales.map((locale) => {
|
||||
if (locale !== routerLocal) defaultPreferredLanguages.push(locale);
|
||||
});
|
||||
} else {
|
||||
defaultPreferredLanguages = [routerLocal, "en"];
|
||||
locales.map((locale) => {
|
||||
if (locale !== routerLocal && locale !== "en") defaultPreferredLanguages.push(locale);
|
||||
});
|
||||
}
|
||||
return defaultPreferredLanguages;
|
||||
};
|
||||
|
||||
export const getPreferredLanguage = (
|
||||
|
|
|
@ -4,11 +4,11 @@ import { getFormat } from "./i18n";
|
|||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
const DEFAULT_OG_THUMBNAIL: OgImage = {
|
||||
const DEFAULT_OG_THUMBNAIL = {
|
||||
image: `${process.env.NEXT_PUBLIC_URL_SELF}/default_og.jpg`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Accord’s Library Logo",
|
||||
alt: "Accord's Library Logo",
|
||||
};
|
||||
|
||||
export const TITLE_PREFIX = "Accord’s Library";
|
||||
|
@ -18,38 +18,20 @@ export interface OpenGraph {
|
|||
title: string;
|
||||
description: string;
|
||||
thumbnail: OgImage;
|
||||
audio?: string;
|
||||
video?: string;
|
||||
}
|
||||
|
||||
export const getOpenGraph = (
|
||||
format: ReturnType<typeof getFormat>["format"] | ReturnType<typeof useFormat>["format"],
|
||||
title?: string | null | undefined,
|
||||
description?: string | null | undefined,
|
||||
thumbnail?: UploadImageFragment | string | null | undefined,
|
||||
audio?: string,
|
||||
video?: string
|
||||
thumbnail?: UploadImageFragment | null | undefined
|
||||
): OpenGraph => ({
|
||||
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? `${TITLE_SEPARATOR}${title}` : ""}`,
|
||||
description: isDefinedAndNotEmpty(description)
|
||||
? description.length > 350
|
||||
? `${description.slice(0, 349)}…`
|
||||
: description
|
||||
: format("default_description"),
|
||||
description: isDefinedAndNotEmpty(description) ? description : format("default_description"),
|
||||
thumbnail: thumbnail ? getOgImage(thumbnail) : DEFAULT_OG_THUMBNAIL,
|
||||
...(audio ? { audio } : {}),
|
||||
...(video ? { video } : {}),
|
||||
});
|
||||
|
||||
const getOgImage = (image: UploadImageFragment | string): OgImage => {
|
||||
if (typeof image === "string") {
|
||||
return {
|
||||
image,
|
||||
width: 0,
|
||||
height: 0,
|
||||
alt: "",
|
||||
};
|
||||
}
|
||||
const getOgImage = (image: UploadImageFragment): OgImage => {
|
||||
const imgSize = getImgSizesByQuality(image.width ?? 0, image.height ?? 0, ImageQuality.Og);
|
||||
return {
|
||||
image: getAssetURL(image.url, ImageQuality.Og),
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { MeiliSearch } from "meilisearch";
|
||||
import type {
|
||||
SearchParams,
|
||||
|
@ -75,6 +73,7 @@ export const filterHitsWithHighlight = <T extends MeiliDocumentsType["documents"
|
|||
return result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const meiliSearch = async <I extends MeiliDocumentsType["index"]>(
|
||||
indexName: I,
|
||||
query: string,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export const getVideoThumbnailURL = (uid: string): string =>
|
||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/videos/${uid}.webp`;
|
||||
`${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`;
|
||||
|
||||
export const getVideoFile = (uid: string): string =>
|
||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/videos/${uid}.mp4`;
|
||||
`${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`;
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import {
|
||||
DatePickerFragment,
|
||||
LibraryItemMetadataDynamicZone,
|
||||
PricePickerFragment,
|
||||
} from "graphql/generated";
|
||||
import { LibraryItemMetadataDynamicZone } from "graphql/generated";
|
||||
import { ICUParams } from "graphql/icuParams";
|
||||
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { getLogger } from "helpers/logger";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { LibraryItemMetadata } from "types/types";
|
||||
import { convertPrice } from "helpers/numbers";
|
||||
import { datePickerToDate } from "helpers/date";
|
||||
|
||||
const logger = getLogger("🗺️ [I18n]");
|
||||
|
||||
|
@ -22,15 +13,6 @@ type WordingKey = keyof ICUParams;
|
|||
type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>;
|
||||
export type ContentStatus = "Done" | "Draft" | "Incomplete" | "Review";
|
||||
|
||||
/*
|
||||
* Whitelisting wording keys from being detected as unused
|
||||
* - format("audio")
|
||||
* - format("textual")
|
||||
* - format("game")
|
||||
* - format("group")
|
||||
* - format("video")
|
||||
* - format("other")
|
||||
*/
|
||||
const componentMetadataToWording: Record<LibraryItemType, WordingKey> = {
|
||||
ComponentMetadataAudio: "audio",
|
||||
ComponentMetadataBooks: "textual",
|
||||
|
@ -41,17 +23,6 @@ const componentMetadataToWording: Record<LibraryItemType, WordingKey> = {
|
|||
Error: "item",
|
||||
};
|
||||
|
||||
/*
|
||||
* Whitelisting wording keys from being detected as unused
|
||||
* - format("draft")
|
||||
* - format("incomplete")
|
||||
* - format("review")
|
||||
* - format("done")
|
||||
* - format("status_draft")
|
||||
* - format("status_incomplete")
|
||||
* - format("status_review")
|
||||
* - format("status_done")
|
||||
*/
|
||||
const componentSetsTextsetStatusToWording: Record<
|
||||
ContentStatus,
|
||||
{ label: WordingKey; description: WordingKey }
|
||||
|
@ -67,27 +38,12 @@ export const useFormat = (): {
|
|||
key: K,
|
||||
...values: ICUParams[K] extends never ? [undefined?] : [ICUParams[K]]
|
||||
) => string;
|
||||
formatLibraryItemType: (metadata: LibraryItemMetadata) => string;
|
||||
formatLibraryItemSubType: (metadata: LibraryItemMetadata) => string;
|
||||
formatLibraryItemType: (metadata: { __typename: LibraryItemType }) => string;
|
||||
formatStatusLabel: (status: ContentStatus) => string;
|
||||
formatStatusDescription: (status: ContentStatus) => string;
|
||||
formatCategory: (slug: string, type?: "default" | "full") => string;
|
||||
formatContentType: (slug: string) => string;
|
||||
formatWikiTag: (slug: string) => string;
|
||||
formatWeaponType: (slug: string) => string;
|
||||
formatLanguage: (code: string) => string;
|
||||
formatPrice: (price: PricePickerFragment, targetCurrencyCode?: string) => string;
|
||||
formatDate: (
|
||||
datePicker: DatePickerFragment,
|
||||
dateStyle?: Intl.DateTimeFormatOptions["dateStyle"]
|
||||
) => string;
|
||||
} => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const fallbackLangui = useAtomGetter(atoms.localData.fallbackLangui);
|
||||
const typesTranslations = useAtomGetter(atoms.localData.typesTranslations);
|
||||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||
const { locale = "en" } = useRouter();
|
||||
|
||||
const format = useCallback(
|
||||
(
|
||||
|
@ -120,6 +76,12 @@ Falling back to en translation.`
|
|||
[langui, fallbackLangui]
|
||||
);
|
||||
|
||||
const formatLibraryItemType = useCallback(
|
||||
(metadata: { __typename: LibraryItemType }): string =>
|
||||
format(componentMetadataToWording[metadata.__typename]),
|
||||
[format]
|
||||
);
|
||||
|
||||
const formatStatusLabel = useCallback(
|
||||
(status: ContentStatus): string => format(componentSetsTextsetStatusToWording[status].label),
|
||||
[format]
|
||||
|
@ -131,219 +93,10 @@ Falling back to en translation.`
|
|||
[format]
|
||||
);
|
||||
|
||||
const formatLibraryItemType = useCallback(
|
||||
(metadata: LibraryItemMetadata): string =>
|
||||
metadata ? format(componentMetadataToWording[metadata.__typename]) : format("other"),
|
||||
[format]
|
||||
);
|
||||
|
||||
const formatLibraryItemSubType = useCallback(
|
||||
(metadata: LibraryItemMetadata): string => {
|
||||
switch (metadata?.__typename) {
|
||||
case "ComponentMetadataAudio": {
|
||||
const slug = metadata.subtype?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.audioSubtypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataBooks": {
|
||||
const slug = metadata.subtype?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.textualSubtypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataVideo": {
|
||||
const slug = metadata.subtype?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.videoSubtypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataGame": {
|
||||
const slug = metadata.platform?.data?.attributes?.slug;
|
||||
const subtype = typesTranslations.gamePlatforms.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subtype?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
}
|
||||
|
||||
case "ComponentMetadataGroup": {
|
||||
const subItemType = (() => {
|
||||
const subitemTypeSlug = metadata.subitems_type?.data?.attributes?.slug;
|
||||
const subItemTypeTranslations = typesTranslations.metadataTypes.find(
|
||||
(type) => type.attributes?.slug === subitemTypeSlug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
subItemTypeTranslations?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
})();
|
||||
const groupType = (() => {
|
||||
const groupTypeSlug = metadata.subtype?.data?.attributes?.slug;
|
||||
const groupTypeTranslations = typesTranslations.groupSubtypes.find(
|
||||
(type) => type.attributes?.slug === groupTypeSlug
|
||||
);
|
||||
const findTranslation = (givenLocale: string) =>
|
||||
groupTypeTranslations?.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
)?.title;
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? format("other");
|
||||
})();
|
||||
return `${groupType} - ${subItemType}`;
|
||||
}
|
||||
|
||||
default:
|
||||
return format("other");
|
||||
}
|
||||
},
|
||||
[
|
||||
format,
|
||||
locale,
|
||||
typesTranslations.audioSubtypes,
|
||||
typesTranslations.gamePlatforms,
|
||||
typesTranslations.groupSubtypes,
|
||||
typesTranslations.metadataTypes,
|
||||
typesTranslations.textualSubtypes,
|
||||
typesTranslations.videoSubtypes,
|
||||
]
|
||||
);
|
||||
|
||||
const formatCategory = useCallback(
|
||||
(slug: string, type: "default" | "full" = "default"): string => {
|
||||
const category = typesTranslations.categories.find((cat) => cat.attributes?.slug === slug);
|
||||
if (!category) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = category.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return type === "full" ? localeTranslation?.title : localeTranslation?.short;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
},
|
||||
[locale, typesTranslations.categories]
|
||||
);
|
||||
|
||||
const formatContentType = useCallback(
|
||||
(slug: string): string => {
|
||||
const contentType = typesTranslations.contentTypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
if (!contentType) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = contentType.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return localeTranslation?.title;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
},
|
||||
[locale, typesTranslations.contentTypes]
|
||||
);
|
||||
|
||||
const formatWikiTag = useCallback(
|
||||
(slug: string): string => {
|
||||
const wikiTag = typesTranslations.wikiPagesTags.find((cat) => cat.attributes?.slug === slug);
|
||||
if (!wikiTag) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = wikiTag.attributes?.titles?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return localeTranslation?.title;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
},
|
||||
[locale, typesTranslations.wikiPagesTags]
|
||||
);
|
||||
|
||||
const formatWeaponType = useCallback(
|
||||
(slug: string): string => {
|
||||
const weaponType = typesTranslations.weaponTypes.find(
|
||||
(type) => type.attributes?.slug === slug
|
||||
);
|
||||
if (!weaponType) return prettySlug(slug);
|
||||
const findTranslation = (givenLocale: string): string | null | undefined => {
|
||||
const localeTranslation = weaponType.attributes?.translations?.find(
|
||||
(translation) => translation?.language?.data?.attributes?.code === givenLocale
|
||||
);
|
||||
return localeTranslation?.name;
|
||||
};
|
||||
return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug);
|
||||
},
|
||||
[locale, typesTranslations.weaponTypes]
|
||||
);
|
||||
|
||||
const formatLanguage = useCallback(
|
||||
(code: string) =>
|
||||
languages.find((language) => language.attributes?.code === code)?.attributes
|
||||
?.localized_name ?? code.toUpperCase(),
|
||||
[languages]
|
||||
);
|
||||
|
||||
const formatPrice = useCallback(
|
||||
(price: PricePickerFragment, targetCurrencyCode?: string) => {
|
||||
if (isUndefined(price.amount)) return "";
|
||||
|
||||
const targetCurrency = currencies.find(
|
||||
(currency) => currency.attributes?.code === targetCurrencyCode
|
||||
);
|
||||
|
||||
if (targetCurrency?.attributes) {
|
||||
const amountInTargetCurrency = convertPrice(price, targetCurrency);
|
||||
return amountInTargetCurrency.toLocaleString(locale, {
|
||||
style: "currency",
|
||||
currency: targetCurrency.attributes.code,
|
||||
});
|
||||
}
|
||||
return price.amount.toLocaleString(locale, {
|
||||
style: "currency",
|
||||
currency: price.currency?.data?.attributes?.code,
|
||||
});
|
||||
},
|
||||
[currencies, locale]
|
||||
);
|
||||
|
||||
const formatDate = useCallback(
|
||||
(
|
||||
datePicker: DatePickerFragment,
|
||||
dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium"
|
||||
) => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle }),
|
||||
[locale]
|
||||
);
|
||||
|
||||
return {
|
||||
format,
|
||||
formatLibraryItemType,
|
||||
formatLibraryItemSubType,
|
||||
formatStatusLabel,
|
||||
formatStatusDescription,
|
||||
formatCategory,
|
||||
formatContentType,
|
||||
formatWikiTag,
|
||||
formatWeaponType,
|
||||
formatLanguage,
|
||||
formatPrice,
|
||||
formatDate,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ export const useFullscreen = (
|
|||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const isClient = useIsClient();
|
||||
|
||||
const elem = isClient ? document.querySelector(`#${CSS.escape(id)}`) : null;
|
||||
const elem = isClient ? document.querySelector(`#${id}`) : null;
|
||||
|
||||
const requestFullscreen = useCallback(async () => elem?.requestFullscreen(), [elem]);
|
||||
const exitFullscreen = useCallback(
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { useMemo } from "react";
|
||||
import UAParser from "ua-parser-js";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
|
||||
export const useIsWebkit = (): boolean => {
|
||||
const isClient = useIsClient();
|
||||
return useMemo<boolean>(() => {
|
||||
if (isClient) {
|
||||
const parser = new UAParser();
|
||||
return parser.getBrowser().name === "Safari" || parser.getOS().name === "iOS";
|
||||
}
|
||||
return false;
|
||||
}, [isClient]);
|
||||
};
|
|
@ -13,7 +13,7 @@ export const useOnResize = (
|
|||
|
||||
useEffect(() => {
|
||||
logger.log(`Creating observer for ${id}`);
|
||||
const elem = isClient ? document.querySelector(`#${CSS.escape(id)}`) : null;
|
||||
const elem = isClient ? document.querySelector(`#${id}`) : null;
|
||||
const ro = new ResizeObserver((resizeObserverEntry) => {
|
||||
const entry = resizeObserverEntry[0];
|
||||
if (isDefined(entry)) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Ids } from "types/ids";
|
|||
|
||||
export const useOnScroll = (id: Ids, onScroll: (scroll: number) => void): void => {
|
||||
const isClient = useIsClient();
|
||||
const elem = isClient ? document.querySelector(`#${CSS.escape(id)}`) : null;
|
||||
const elem = isClient ? document.querySelector(`#${id}`) : null;
|
||||
const listener = useCallback(() => {
|
||||
if (elem?.scrollTop) {
|
||||
onScroll(elem.scrollTop);
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { DependencyList, useEffect } from "react";
|
||||
import { getLogger } from "helpers/logger";
|
||||
import { Ids } from "types/ids";
|
||||
|
||||
const logger = getLogger("⬆️ [Scroll Top On Change]");
|
||||
|
||||
// Scroll to top of element "id" when "deps" update.
|
||||
export const useScrollTopOnChange = (id: Ids, deps: DependencyList, enabled = true): void => {
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
logger.log("Change detected. Scrolling to top");
|
||||
document.querySelector(`#${CSS.escape(id)}`)?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, ...deps, enabled]);
|
||||
};
|
||||
|
||||
// Scroll to top of element "id" when "deps" update.
|
||||
export const useScrollRightOnChange = (id: Ids, deps: DependencyList, enabled = true): void => {
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
logger.log("Change detected. Scrolling to right");
|
||||
const elem = document.querySelector(`#${CSS.escape(id)}`);
|
||||
elem?.scrollTo({ left: elem.scrollWidth, behavior: "smooth" });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, ...deps, enabled]);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { DependencyList, useEffect } from "react";
|
||||
import { getLogger } from "helpers/logger";
|
||||
import { Ids } from "types/ids";
|
||||
|
||||
const logger = getLogger("⬆️ [Scroll Top On Change]");
|
||||
|
||||
// Scroll to top of element "id" when "deps" update.
|
||||
export const useScrollTopOnChange = (id: Ids, deps: DependencyList, enabled = true): void => {
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
logger.log("Change detected. Scrolling to top");
|
||||
document.querySelector(`#${id}`)?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, ...deps, enabled]);
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
import { filterDefined, isDefined } from "helpers/asserts";
|
||||
import { getPreferredLanguage } from "helpers/locales";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
|
@ -21,18 +21,14 @@ export const useSmartLanguage = <T>({
|
|||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
const router = useRouter();
|
||||
|
||||
const availableLocales = useMemo<Map<string, number>>(() => {
|
||||
const memo: [string, number][] = [];
|
||||
items.map((elem, index) => {
|
||||
const result = isDefined(elem) ? languageExtractor(elem) : undefined;
|
||||
if (isDefined(result)) memo.push([result, index]);
|
||||
const availableLocales = useMemo(() => {
|
||||
const memo = new Map<string, number>();
|
||||
filterDefined(items).map((elem, index) => {
|
||||
const result = languageExtractor(elem);
|
||||
if (isDefined(result)) memo.set(result, index);
|
||||
});
|
||||
memo.sort((a, b) => {
|
||||
const evaluate = (locale: string) => preferredLanguages.findIndex((elem) => elem === locale);
|
||||
return evaluate(a[0]) - evaluate(b[0]);
|
||||
});
|
||||
return new Map(memo);
|
||||
}, [items, languageExtractor, preferredLanguages]);
|
||||
return memo;
|
||||
}, [items, languageExtractor]);
|
||||
|
||||
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<number | undefined>();
|
||||
|
||||
|
|
|
@ -14,31 +14,28 @@ import "styles/formatted.css";
|
|||
import "styles/others.css";
|
||||
import "styles/rc-slider.css";
|
||||
import "styles/tippy.css";
|
||||
import "styles/fonts.css";
|
||||
|
||||
import { useLocalData } from "contexts/localData";
|
||||
import { LightBoxProvider } from "contexts/LightBoxProvider";
|
||||
import { SettingsPopup } from "components/Panels/SettingsPopup";
|
||||
import { useSettings } from "contexts/settings";
|
||||
import { useContainerQueries } from "contexts/containerQueries";
|
||||
import { useWebkitFixes } from "contexts/webkitFixes";
|
||||
import { SearchPopup } from "components/Panels/SearchPopup";
|
||||
import { useScrollIntoView } from "hooks/useScrollIntoView";
|
||||
import { useUserAgent } from "contexts/userAgent";
|
||||
import { DebugPopup } from "components/Panels/DebugPopup";
|
||||
|
||||
const AccordsLibraryApp = (props: AppProps): JSX.Element => {
|
||||
useLocalData();
|
||||
useSettings();
|
||||
useContainerQueries();
|
||||
useWebkitFixes();
|
||||
useScrollIntoView();
|
||||
useUserAgent();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchPopup />
|
||||
<SettingsPopup />
|
||||
<LightBoxProvider />
|
||||
<DebugPopup />
|
||||
<Script
|
||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
|
||||
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/script.js`}
|
||||
|
|
|
@ -10,7 +10,6 @@ import { sendAnalytics } from "helpers/analytics";
|
|||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -147,10 +146,10 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
<input
|
||||
type="submit"
|
||||
value={format("send")}
|
||||
className="w-min !px-6"
|
||||
text={format("send")}
|
||||
disabled={formState !== "stale"}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,13 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { i18n } from "../../../next.config";
|
||||
import { cartesianProduct } from "helpers/others";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import {
|
||||
fetchCurrencies,
|
||||
fetchLanguages,
|
||||
fetchRecorders,
|
||||
fetchTypesTranslations,
|
||||
fetchWebsiteInterfaces,
|
||||
} from "graphql/fetchLocalData";
|
||||
import { fetchLocalData } from "graphql/fetchLocalData";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
|
||||
type CRUDEvents = "entry.create" | "entry.delete" | "entry.update";
|
||||
|
@ -26,32 +20,21 @@ type StrapiRelationalFieldEntry = {
|
|||
|
||||
type RequestProps =
|
||||
| CustomRequest
|
||||
| StrapiAudioSubType
|
||||
| StrapiCategory
|
||||
| StrapiChronicle
|
||||
| StrapiChronicleChapter
|
||||
| StrapiChronology
|
||||
| StrapiContent
|
||||
| StrapiContentFolder
|
||||
| StrapiContentType
|
||||
| StrapiCurrency
|
||||
| StrapiGamePlatform
|
||||
| StrapiGroupSubtypes
|
||||
| StrapiLanguage
|
||||
| StrapiLibraryItem
|
||||
| StrapiMetadataType
|
||||
| StrapiPostContent
|
||||
| StrapiRangedContent
|
||||
| StrapiRecorder
|
||||
| StrapiTextualSubtypes
|
||||
| StrapiVideo
|
||||
| StrapiVideoSubType
|
||||
| StrapiWeaponGroup
|
||||
| StrapiWeaponStory
|
||||
| StrapiWeaponStoryType
|
||||
| StrapiWebsiteInterface
|
||||
| StrapiWiki
|
||||
| StrapiWikiPagesTag;
|
||||
| StrapiWiki;
|
||||
|
||||
interface CustomRequest {
|
||||
model: "custom";
|
||||
|
@ -74,6 +57,7 @@ interface StrapiWeaponGroup extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiRangedContent extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "ranged-content";
|
||||
entry: {
|
||||
id: string;
|
||||
|
@ -94,6 +78,7 @@ interface StrapiContent extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiPostContent extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "post";
|
||||
entry: {
|
||||
slug: string;
|
||||
|
@ -101,62 +86,31 @@ interface StrapiPostContent extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiWebsiteInterface extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "website-interface";
|
||||
}
|
||||
|
||||
interface StrapiRecorder extends StrapiEvent {
|
||||
model: "recorder";
|
||||
}
|
||||
|
||||
interface StrapiMetadataType extends StrapiEvent {
|
||||
model: "metadata-type";
|
||||
}
|
||||
|
||||
interface StrapiAudioSubType extends StrapiEvent {
|
||||
model: "audio-subtype";
|
||||
}
|
||||
|
||||
interface StrapiVideoSubType extends StrapiEvent {
|
||||
model: "video-subtype";
|
||||
}
|
||||
|
||||
interface StrapiTextualSubtypes extends StrapiEvent {
|
||||
model: "textual-subtype";
|
||||
}
|
||||
|
||||
interface StrapiGroupSubtypes extends StrapiEvent {
|
||||
model: "group-subtype";
|
||||
}
|
||||
|
||||
interface StrapiGamePlatform extends StrapiEvent {
|
||||
model: "game-platform";
|
||||
}
|
||||
|
||||
interface StrapiContentType extends StrapiEvent {
|
||||
model: "content-type";
|
||||
}
|
||||
|
||||
interface StrapiWikiPagesTag extends StrapiEvent {
|
||||
model: "wiki-pages-tag";
|
||||
}
|
||||
|
||||
interface StrapiWeaponStoryType extends StrapiEvent {
|
||||
model: "weapon-story-type";
|
||||
}
|
||||
|
||||
interface StrapiCategory extends StrapiEvent {
|
||||
model: "category";
|
||||
entry: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface StrapiLanguage extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "language";
|
||||
entry: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface StrapiCurrency extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "currency";
|
||||
entry: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface StrapiLibraryItem extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "library-item";
|
||||
entry: {
|
||||
slug: string;
|
||||
|
@ -167,6 +121,7 @@ interface StrapiLibraryItem extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiContentFolder extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "contents-folder";
|
||||
entry: {
|
||||
slug: string;
|
||||
|
@ -177,10 +132,12 @@ interface StrapiContentFolder extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiChronology extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "chronology-era" | "chronology-item";
|
||||
}
|
||||
|
||||
interface StrapiWiki extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "wiki-page";
|
||||
entry: {
|
||||
slug: string;
|
||||
|
@ -188,6 +145,7 @@ interface StrapiWiki extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiChronicle extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "chronicle";
|
||||
entry: {
|
||||
slug: string;
|
||||
|
@ -195,6 +153,7 @@ interface StrapiChronicle extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiChronicleChapter extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "chronicles-chapter";
|
||||
entry: {
|
||||
chronicles: StrapiRelationalFieldEntry[];
|
||||
|
@ -202,6 +161,7 @@ interface StrapiChronicleChapter extends StrapiEvent {
|
|||
}
|
||||
|
||||
interface StrapiVideo extends StrapiEvent {
|
||||
event: CRUDEvents;
|
||||
model: "video";
|
||||
entry: {
|
||||
uid: string;
|
||||
|
@ -309,7 +269,6 @@ const Revalidate = async (
|
|||
const contentSlug = body.entry.content?.slug;
|
||||
if (libraryItemSlug) {
|
||||
paths.push(`/library/${libraryItemSlug}`);
|
||||
paths.push(`/library/${libraryItemSlug}/reader`);
|
||||
}
|
||||
if (contentSlug) {
|
||||
paths.push(`/contents/${contentSlug}`);
|
||||
|
@ -362,6 +321,13 @@ const Revalidate = async (
|
|||
break;
|
||||
}
|
||||
|
||||
case "website-interface":
|
||||
case "language":
|
||||
case "currency": {
|
||||
await fetchLocalData();
|
||||
break;
|
||||
}
|
||||
|
||||
case "video": {
|
||||
if (body.entry.uid) {
|
||||
paths.push(`/archives/videos/v/${body.entry.uid}`);
|
||||
|
@ -384,38 +350,6 @@ const Revalidate = async (
|
|||
break;
|
||||
}
|
||||
|
||||
case "recorder": {
|
||||
await fetchRecorders();
|
||||
break;
|
||||
}
|
||||
|
||||
case "audio-subtype":
|
||||
case "textual-subtype":
|
||||
case "video-subtype":
|
||||
case "group-subtype":
|
||||
case "game-platform":
|
||||
case "metadata-type":
|
||||
case "content-type":
|
||||
case "weapon-story-type":
|
||||
case "category":
|
||||
case "wiki-pages-tag": {
|
||||
await fetchTypesTranslations();
|
||||
break;
|
||||
}
|
||||
|
||||
case "website-interface": {
|
||||
await fetchWebsiteInterfaces();
|
||||
break;
|
||||
}
|
||||
case "language": {
|
||||
await fetchLanguages();
|
||||
break;
|
||||
}
|
||||
case "currency": {
|
||||
await fetchCurrencies();
|
||||
break;
|
||||
}
|
||||
|
||||
case "custom": {
|
||||
paths.push(`${body.path}`);
|
||||
break;
|
||||
|
@ -438,6 +372,7 @@ const Revalidate = async (
|
|||
})
|
||||
);
|
||||
res.json({ message: "Success!", revalidated: true });
|
||||
return;
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: `Error revalidating: ${error}`, revalidated: false });
|
||||
}
|
||||
|
|
|
@ -22,12 +22,11 @@ import { useTypedRouter } from "hooks/useTypedRouter";
|
|||
import { Select } from "components/Inputs/Select";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { GetVideoChannelQuery } from "graphql/generated";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { Paginator } from "components/Containers/Paginator";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -55,18 +54,15 @@ const queryParamSchema = z.object({
|
|||
*/
|
||||
|
||||
interface Props extends AppLayoutRequired {
|
||||
channel: {
|
||||
uid: string;
|
||||
title: string;
|
||||
subscribers: number;
|
||||
};
|
||||
channel: NonNullable<
|
||||
NonNullable<GetVideoChannelQuery["videoChannels"]>["data"][number]["attributes"]
|
||||
>;
|
||||
}
|
||||
|
||||
const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const hoverable = useDeviceSupportsHover();
|
||||
const router = useTypedRouter(queryParamSchema);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const sortingMethods = useMemo(
|
||||
() => [
|
||||
|
@ -151,27 +147,14 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady]);
|
||||
|
||||
const searchInput = (
|
||||
<TextInput
|
||||
placeholder={format("search_placeholder")}
|
||||
value={query}
|
||||
onChange={(newQuery) => {
|
||||
setPage(1);
|
||||
setQuery(newQuery);
|
||||
if (isDefinedAndNotEmpty(newQuery)) {
|
||||
sendAnalytics("Videos/Channel", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Videos/Channel", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
{!is1ColumnLayout && (
|
||||
<ReturnButton href="/archives/videos" title={format("videos")} className="mb-10" />
|
||||
)}
|
||||
<ReturnButton
|
||||
href="/archives/videos"
|
||||
title={format("videos")}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<PanelHeader
|
||||
icon="movie"
|
||||
|
@ -183,7 +166,20 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
<HorizontalLine />
|
||||
|
||||
{!is1ColumnLayout && <div className="mb-6">{searchInput}</div>}
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={format("search_title")}
|
||||
value={query}
|
||||
onChange={(newQuery) => {
|
||||
setPage(1);
|
||||
setQuery(newQuery);
|
||||
if (isDefinedAndNotEmpty(newQuery)) {
|
||||
sendAnalytics("Videos", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Videos", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={format("order_by")}>
|
||||
<Select
|
||||
|
@ -194,7 +190,7 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
|||
setPage(1);
|
||||
setSortingMethod(newSort);
|
||||
sendAnalytics(
|
||||
"Videos/Channel",
|
||||
"Videos",
|
||||
`Change sorting method (${
|
||||
sortingMethods.map((item) => item.meiliAttribute)[newSort]
|
||||
})`
|
||||
|
@ -228,7 +224,7 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
|||
setQuery(DEFAULT_FILTERS_STATE.searchName);
|
||||
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("Videos/Channel", "Reset all filters");
|
||||
sendAnalytics("Videos", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
|
@ -236,7 +232,6 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{is1ColumnLayout && <div className="mx-auto mb-12 max-w-lg">{searchInput}</div>}
|
||||
<Paginator page={page} onPageChange={setPage} totalNumberOfPages={videos?.totalPages}>
|
||||
<div
|
||||
className="grid grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-start
|
||||
|
@ -283,23 +278,14 @@ export default Channel;
|
|||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format } = getFormat(context.locale);
|
||||
const videoChannel = (
|
||||
await sdk.getVideoChannel({
|
||||
channel: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
|
||||
})
|
||||
).videoChannels?.data[0]?.attributes;
|
||||
|
||||
if (!videoChannel) return { notFound: true };
|
||||
|
||||
const channel: Props["channel"] = {
|
||||
uid: videoChannel.uid,
|
||||
subscribers: videoChannel.subscribers,
|
||||
title: videoChannel.title,
|
||||
};
|
||||
const channel = await sdk.getVideoChannel({
|
||||
channel: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
|
||||
});
|
||||
if (!channel.videoChannels?.data[0]?.attributes) return { notFound: true };
|
||||
|
||||
const props: Props = {
|
||||
channel,
|
||||
openGraph: getOpenGraph(format, channel.title),
|
||||
channel: channel.videoChannels.data[0].attributes,
|
||||
openGraph: getOpenGraph(format, channel.videoChannels.data[0].attributes.title),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
|
|
|
@ -25,8 +25,6 @@ import { Button } from "components/Inputs/Button";
|
|||
import { Paginator } from "components/Containers/Paginator";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -59,7 +57,6 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
|
|||
const { format } = useFormat();
|
||||
const hoverable = useDeviceSupportsHover();
|
||||
const router = useTypedRouter(queryParamSchema);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const sortingMethods = useMemo(
|
||||
() => [
|
||||
|
@ -144,25 +141,14 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady]);
|
||||
|
||||
const searchInput = (
|
||||
<TextInput
|
||||
placeholder={format("search_placeholder")}
|
||||
value={query}
|
||||
onChange={(newQuery) => {
|
||||
setPage(1);
|
||||
setQuery(newQuery);
|
||||
if (isDefinedAndNotEmpty(newQuery)) {
|
||||
sendAnalytics("Videos", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Videos", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
{!is1ColumnLayout && <ReturnButton href="/archives/" title={"Archives"} className="mb-10" />}
|
||||
<ReturnButton
|
||||
href="/archives/"
|
||||
title={"Archives"}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<PanelHeader
|
||||
icon="movie"
|
||||
|
@ -172,7 +158,20 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
<HorizontalLine />
|
||||
|
||||
{!is1ColumnLayout && <div className="mb-6">{searchInput}</div>}
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={format("search_title")}
|
||||
value={query}
|
||||
onChange={(newQuery) => {
|
||||
setPage(1);
|
||||
setQuery(newQuery);
|
||||
if (isDefinedAndNotEmpty(newQuery)) {
|
||||
sendAnalytics("Videos", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Videos", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={format("order_by")}>
|
||||
<Select
|
||||
|
@ -196,7 +195,6 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
|
|||
<Switch
|
||||
value={onlyShowGone}
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
toggleOnlyShowGone();
|
||||
}}
|
||||
/>
|
||||
|
@ -227,7 +225,6 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{is1ColumnLayout && <div className="mx-auto mb-12 max-w-lg">{searchInput}</div>}
|
||||
<Paginator page={page} onPageChange={setPage} totalNumberOfPages={videos?.totalPages}>
|
||||
<div
|
||||
className="grid grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-start
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
|
@ -9,19 +10,17 @@ import { NavOption } from "components/PanelComponents/NavOption";
|
|||
import { ReturnButton } from "components/PanelComponents/ReturnButton";
|
||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||
import { SubPanel } from "components/Containers/SubPanel";
|
||||
import { Enum_Video_Source } from "graphql/generated";
|
||||
import { GetVideoQuery } from "graphql/generated";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { prettyShortenNumber } from "helpers/formatters";
|
||||
import { filterHasAttributes, isDefined, isUndefined } from "helpers/asserts";
|
||||
import { getVideoFile, getVideoThumbnailURL } from "helpers/videos";
|
||||
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import { getVideoFile } from "helpers/videos";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { VideoPlayer } from "components/Player";
|
||||
import { getDescription } from "helpers/description";
|
||||
import { Markdown } from "components/Markdown/Markdown";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -29,38 +28,25 @@ import { Markdown } from "components/Markdown/Markdown";
|
|||
*/
|
||||
|
||||
interface Props extends AppLayoutRequired {
|
||||
video: {
|
||||
isGone: boolean;
|
||||
uid: string;
|
||||
title: string;
|
||||
description: string;
|
||||
publishedDate: string;
|
||||
views: number;
|
||||
likes: number;
|
||||
source?: Enum_Video_Source;
|
||||
};
|
||||
channel?: {
|
||||
title: string;
|
||||
href: string;
|
||||
subscribers: number;
|
||||
};
|
||||
video: NonNullable<NonNullable<GetVideoQuery["videos"]>["data"][number]["attributes"]>;
|
||||
}
|
||||
|
||||
const Video = ({ video, channel, ...otherProps }: Props): JSX.Element => {
|
||||
const Video = ({ video, ...otherProps }: Props): JSX.Element => {
|
||||
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||
const closeSubPanel = useCallback(() => setSubPanelOpened(false), [setSubPanelOpened]);
|
||||
const { format } = useFormat();
|
||||
const router = useRouter();
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
{!is1ColumnLayout && (
|
||||
<>
|
||||
<ReturnButton href="/archives/videos/" title={format("videos")} />
|
||||
<HorizontalLine />
|
||||
</>
|
||||
)}
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={format("videos")}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<NavOption title={format("video")} url="#video" border onClick={closeSubPanel} />
|
||||
<NavOption title={format("channel")} url="#channel" border onClick={closeSubPanel} />
|
||||
|
@ -70,14 +56,17 @@ const Video = ({ video, channel, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{is1ColumnLayout && (
|
||||
<ReturnButton href="/library/" title={format("library")} className="mb-10" />
|
||||
)}
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={format("library")}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
|
||||
{video.isGone ? (
|
||||
<VideoPlayer className="w-full" src={getVideoFile(video.uid)} rounded={false} />
|
||||
{video.gone ? (
|
||||
<video className="w-full" src={getVideoFile(video.uid)} controls />
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
||||
|
@ -91,10 +80,10 @@ const Video = ({ video, channel, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
<div className="mt-2 p-6">
|
||||
<h1 className="text-2xl">{video.title}</h1>
|
||||
<div className="flex w-full flex-row flex-wrap place-items-center gap-x-6">
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-6">
|
||||
<p>
|
||||
<Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{video.publishedDate}
|
||||
{prettyDate(video.published_date, router.locale)}
|
||||
</p>
|
||||
<p>
|
||||
<Ico icon="visibility" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
|
@ -102,7 +91,7 @@ const Video = ({ video, channel, ...otherProps }: Props): JSX.Element => {
|
|||
? video.views.toLocaleString()
|
||||
: prettyShortenNumber(video.views)}
|
||||
</p>
|
||||
{video.likes > 0 && (
|
||||
{video.channel?.data?.attributes && (
|
||||
<p>
|
||||
<Ico icon="thumb_up" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{isContentPanelAtLeast4xl
|
||||
|
@ -110,26 +99,25 @@ const Video = ({ video, channel, ...otherProps }: Props): JSX.Element => {
|
|||
: prettyShortenNumber(video.likes)}
|
||||
</p>
|
||||
)}
|
||||
{video.source === "YouTube" && (
|
||||
<Button
|
||||
size="small"
|
||||
text={format("view_on_x", { x: video.source })}
|
||||
href={`https://youtu.be/${video.uid}`}
|
||||
alwaysNewTab
|
||||
/>
|
||||
)}
|
||||
<Link href={`https://youtu.be/${video.uid}`} alwaysNewTab>
|
||||
<Button size="small" text={`${format("view_on")} ${video.source}`} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{channel && (
|
||||
{video.channel?.data?.attributes && (
|
||||
<InsetBox id="channel" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center">
|
||||
<h2 className="text-2xl">{format("channel")}</h2>
|
||||
<div>
|
||||
<Button href={channel.href} text={channel.title} />
|
||||
<Button
|
||||
href={`/archives/videos/c/${video.channel.data.attributes.uid}\
|
||||
?page=1&query=&sort=1&gone=`}
|
||||
text={video.channel.data.attributes.title}
|
||||
/>
|
||||
<p>
|
||||
{`${channel.subscribers.toLocaleString()}
|
||||
{`${video.channel.data.attributes.subscribers.toLocaleString()}
|
||||
${format("subscribers").toLowerCase()}`}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -140,7 +128,7 @@ const Video = ({ video, channel, ...otherProps }: Props): JSX.Element => {
|
|||
<InsetBox id="description" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
<h2 className="text-2xl">{format("description")}</h2>
|
||||
<Markdown className="whitespace-pre-line" text={video.description} />
|
||||
<p className="whitespace-pre-line">{video.description}</p>
|
||||
</div>
|
||||
</InsetBox>
|
||||
</div>
|
||||
|
@ -158,48 +146,18 @@ export default Video;
|
|||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format, formatDate } = getFormat(context.locale);
|
||||
const { format } = getFormat(context.locale);
|
||||
const videos = await sdk.getVideo({
|
||||
uid: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
|
||||
});
|
||||
const rawVideo = videos.videos?.data[0]?.attributes;
|
||||
if (isUndefined(rawVideo)) return { notFound: true };
|
||||
|
||||
const channel: Props["channel"] = rawVideo.channel?.data?.attributes
|
||||
? {
|
||||
href: `/archives/videos/c/${rawVideo.channel.data.attributes.uid}`,
|
||||
subscribers: rawVideo.channel.data.attributes.subscribers,
|
||||
title: rawVideo.channel.data.attributes.title,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const video: Props["video"] = {
|
||||
uid: rawVideo.uid,
|
||||
isGone: rawVideo.gone,
|
||||
description: rawVideo.description,
|
||||
likes: rawVideo.likes,
|
||||
source: rawVideo.source ?? undefined,
|
||||
publishedDate: formatDate(rawVideo.published_date),
|
||||
title: rawVideo.title,
|
||||
views: rawVideo.views,
|
||||
};
|
||||
if (!videos.videos?.data[0]?.attributes) return { notFound: true };
|
||||
|
||||
const props: Props = {
|
||||
video,
|
||||
channel,
|
||||
openGraph: getOpenGraph(
|
||||
format,
|
||||
rawVideo.title,
|
||||
getDescription(rawVideo.description, {
|
||||
[format("channel")]: [rawVideo.channel?.data?.attributes?.title],
|
||||
}),
|
||||
getVideoThumbnailURL(rawVideo.uid),
|
||||
undefined,
|
||||
rawVideo.gone ? getVideoFile(rawVideo.uid) : undefined
|
||||
),
|
||||
video: videos.videos.data[0].attributes,
|
||||
openGraph: getOpenGraph(format, videos.videos.data[0].attributes.title),
|
||||
};
|
||||
return {
|
||||
props: JSON.parse(JSON.stringify(props)),
|
||||
props: props,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -16,14 +16,12 @@ import { ReturnButton } from "components/PanelComponents/ReturnButton";
|
|||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||
import { getDescription } from "helpers/description";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollOnChange";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { Ids } from "types/ids";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { ElementsSeparator } from "helpers/component";
|
||||
import { ChroniclesLists } from "components/Chronicles/ChroniclesLists";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -36,8 +34,7 @@ interface Props extends AppLayoutRequired {
|
|||
}
|
||||
|
||||
const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element => {
|
||||
const { format, formatContentType, formatCategory } = useFormat();
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
const { format } = useFormat();
|
||||
useScrollTopOnChange(Ids.ContentPanel, [chronicle.slug]);
|
||||
|
||||
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||
|
@ -70,22 +67,24 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element =
|
|||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
{!is1ColumnLayout && (
|
||||
<>
|
||||
<ReturnButton href="/chronicles" title={format("chronicles")} />
|
||||
<HorizontalLine />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ReturnButton
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
href="/chronicles"
|
||||
title={format("chronicles")}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<ChroniclesLists chapters={chapters} currentChronicleSlug={chronicle.slug} />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
{is1ColumnLayout && (
|
||||
<ReturnButton href="/chronicles" title={format("chronicles")} className="mb-10" />
|
||||
)}
|
||||
<ReturnButton
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
href="/chronicles"
|
||||
title={format("chronicles")}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{isDefined(selectedTranslation) ? (
|
||||
<>
|
||||
|
@ -112,14 +111,8 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element =
|
|||
<ContentLanguageSwitcher {...ContentLanguageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
categories={filterHasAttributes(primaryContent?.categories?.data, [
|
||||
"attributes",
|
||||
]).map((category) => formatCategory(category.attributes.slug))}
|
||||
type={
|
||||
primaryContent?.type?.data?.attributes
|
||||
? formatContentType(primaryContent.type.data.attributes.slug)
|
||||
: undefined
|
||||
}
|
||||
categories={primaryContent?.categories}
|
||||
type={primaryContent?.type}
|
||||
description={selectedContentTranslation.description}
|
||||
thumbnail={primaryContent?.thumbnail?.data?.attributes}
|
||||
/>,
|
||||
|
@ -153,10 +146,13 @@ export default Chronicle;
|
|||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format, formatCategory, formatContentType } = getFormat(context.locale);
|
||||
const { format } = getFormat(context.locale);
|
||||
const slug =
|
||||
context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
|
||||
const chronicle = await sdk.getChronicle({ slug: slug });
|
||||
const chronicle = await sdk.getChronicle({
|
||||
language_code: context.locale ?? "en",
|
||||
slug: slug,
|
||||
});
|
||||
const chronicles = await sdk.getChroniclesChapters();
|
||||
if (
|
||||
!chronicle.chronicles?.data[0]?.attributes?.translations ||
|
||||
|
@ -182,18 +178,13 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
description: getDescription(selectedContentTranslation.description, {
|
||||
[format("type", { count: Infinity })]: [
|
||||
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.type?.data
|
||||
?.attributes
|
||||
? formatContentType(
|
||||
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.type.data
|
||||
.attributes.slug
|
||||
)
|
||||
: undefined,
|
||||
?.attributes?.titles?.[0]?.title,
|
||||
],
|
||||
[format("category", { count: Infinity })]: filterHasAttributes(
|
||||
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.categories
|
||||
?.data,
|
||||
["attributes"]
|
||||
).map((category) => formatCategory(category.attributes.slug)),
|
||||
).map((category) => category.attributes.short),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Fragment, useCallback, useState } from "react";
|
||||
import Collapsible from "react-collapsible";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Chip } from "components/Chip";
|
||||
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
|
||||
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "components/Markdown/Markdawn";
|
||||
import { TranslatedReturnButton } from "components/PanelComponents/ReturnButton";
|
||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||
import { SubPanel } from "components/Containers/SubPanel";
|
||||
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
|
||||
import { RecorderChip } from "components/RecorderChip";
|
||||
import { ThumbnailHeader } from "components/ThumbnailHeader";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
|
||||
import {
|
||||
prettyInlineTitle,
|
||||
prettyLanguage,
|
||||
prettyItemSubType,
|
||||
prettySlug,
|
||||
} from "helpers/formatters";
|
||||
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
||||
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { ContentWithTranslations } from "types/types";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollOnChange";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||
|
@ -28,18 +36,12 @@ import { getFormat } from "helpers/i18n";
|
|||
import { ElementsSeparator } from "helpers/component";
|
||||
import { RelatedContentPreviewFragment } from "graphql/generated";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { ButtonGroup, ButtonGroupProps } from "components/Inputs/ButtonGroup";
|
||||
import { AudioPlayer, VideoPlayer } from "components/Player";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Credits } from "components/Credits";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
type SetType = "audio_set" | "text_set" | "video_set";
|
||||
|
||||
interface Props extends AppLayoutRequired {
|
||||
content: ContentWithTranslations;
|
||||
}
|
||||
|
@ -47,7 +49,9 @@ interface Props extends AppLayoutRequired {
|
|||
const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
const { format, formatCategory, formatLibraryItemSubType, formatContentType } = useFormat();
|
||||
|
||||
const { format, formatStatusDescription } = useFormat();
|
||||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
|
||||
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||
items: content.translations,
|
||||
|
@ -59,20 +63,6 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|||
});
|
||||
|
||||
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
|
||||
const [selectedSetType, setSelectedSetType] = useState<SetType>();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(selectedSetType) && selectedTranslation?.[selectedSetType]) return;
|
||||
if (selectedTranslation?.text_set) {
|
||||
setSelectedSetType("text_set");
|
||||
} else if (selectedTranslation?.audio_set) {
|
||||
setSelectedSetType("audio_set");
|
||||
} else if (selectedTranslation?.video_set) {
|
||||
setSelectedSetType("video_set");
|
||||
} else {
|
||||
setSelectedSetType(undefined);
|
||||
}
|
||||
}, [selectedSetType, selectedTranslation]);
|
||||
|
||||
const returnButtonProps = {
|
||||
href: content.folder?.data?.attributes
|
||||
|
@ -101,73 +91,104 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|||
)
|
||||
);
|
||||
|
||||
const setTypeSelectorProps: ButtonGroupProps["buttonsProps"] = [
|
||||
{
|
||||
text: "Text",
|
||||
icon: "subject",
|
||||
visible: isDefined(selectedTranslation?.text_set),
|
||||
onClick: () => setSelectedSetType("text_set"),
|
||||
active: selectedSetType === "text_set",
|
||||
},
|
||||
{
|
||||
text: "Audio",
|
||||
icon: "headphones",
|
||||
visible: isDefined(selectedTranslation?.audio_set),
|
||||
onClick: () => setSelectedSetType("audio_set"),
|
||||
active: selectedSetType === "audio_set",
|
||||
},
|
||||
{
|
||||
text: "Video",
|
||||
icon: "movie",
|
||||
visible: isDefined(selectedTranslation?.video_set),
|
||||
onClick: () => setSelectedSetType("video_set"),
|
||||
active: selectedSetType === "video_set",
|
||||
},
|
||||
];
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ElementsSeparator>
|
||||
{[
|
||||
!is1ColumnLayout && <TranslatedReturnButton {...returnButtonProps} />,
|
||||
|
||||
selectedSetType === "text_set" ? (
|
||||
<Credits
|
||||
key="credits"
|
||||
languageCode={selectedTranslation?.language?.data?.attributes?.code}
|
||||
sourceLanguageCode={
|
||||
selectedTranslation?.text_set?.source_language?.data?.attributes?.code
|
||||
}
|
||||
status={selectedTranslation?.text_set?.status}
|
||||
transcribers={selectedTranslation?.text_set?.transcribers?.data}
|
||||
translators={selectedTranslation?.text_set?.translators?.data}
|
||||
proofreaders={selectedTranslation?.text_set?.proofreaders?.data}
|
||||
notes={selectedTranslation?.text_set?.notes}
|
||||
/>
|
||||
) : selectedSetType === "audio_set" ? (
|
||||
<Credits
|
||||
key="credits"
|
||||
languageCode={selectedTranslation?.language?.data?.attributes?.code}
|
||||
sourceLanguageCode={
|
||||
selectedTranslation?.audio_set?.source_language?.data?.attributes?.code
|
||||
}
|
||||
status={selectedTranslation?.audio_set?.status}
|
||||
dubbers={selectedTranslation?.audio_set?.dubbers?.data}
|
||||
notes={selectedTranslation?.audio_set?.notes}
|
||||
/>
|
||||
) : (
|
||||
selectedSetType === "video_set" && (
|
||||
<Credits
|
||||
key="credits"
|
||||
languageCode={selectedTranslation?.language?.data?.attributes?.code}
|
||||
sourceLanguageCode={
|
||||
selectedTranslation?.video_set?.source_language?.data?.attributes?.code
|
||||
}
|
||||
status={selectedTranslation?.video_set?.status}
|
||||
subbers={selectedTranslation?.video_set?.subbers?.data}
|
||||
notes={selectedTranslation?.video_set?.notes}
|
||||
/>
|
||||
)
|
||||
selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
|
||||
<div className="grid gap-5">
|
||||
<h2 className="text-xl">
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code ===
|
||||
selectedTranslation.language?.data?.attributes?.code
|
||||
? format("transcript_notice")
|
||||
: format("translation_notice")}
|
||||
</h2>
|
||||
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code !==
|
||||
selectedTranslation.language?.data?.attributes?.code && (
|
||||
<div className="grid place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{format("source_language")}:</p>
|
||||
<Chip
|
||||
text={prettyLanguage(
|
||||
selectedTranslation.text_set.source_language.data.attributes.code,
|
||||
languages
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{format("status")}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={formatStatusDescription(selectedTranslation.text_set.status)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.text_set.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{selectedTranslation.text_set.transcribers &&
|
||||
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{format("transcribers")}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
]).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.translators &&
|
||||
selectedTranslation.text_set.translators.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{format("translators")}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
]).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.proofreaders &&
|
||||
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{format("proofreaders")}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
]).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{format("notes")}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
<Markdawn text={selectedTranslation.text_set.notes} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
toc && <TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />,
|
||||
|
@ -196,12 +217,12 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|||
libraryItem.attributes.metadata &&
|
||||
libraryItem.attributes.metadata.length > 0 &&
|
||||
libraryItem.attributes.metadata[0]
|
||||
? [formatLibraryItemSubType(libraryItem.attributes.metadata[0])]
|
||||
? [prettyItemSubType(libraryItem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [
|
||||
"attributes",
|
||||
]).map((category) => formatCategory(category.attributes.slug))}
|
||||
]).map((category) => category.attributes.short)}
|
||||
metadata={{
|
||||
releaseDate: libraryItem.attributes.release_date,
|
||||
price: libraryItem.attributes.price,
|
||||
|
@ -226,44 +247,31 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{is1ColumnLayout && <TranslatedReturnButton {...returnButtonProps} className="mb-10" />}
|
||||
<TranslatedReturnButton
|
||||
{...returnButtonProps}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<ElementsSeparator
|
||||
separator={
|
||||
selectedSetType === "text_set" ? (
|
||||
<HorizontalLine className="max-w-2xl" />
|
||||
) : (
|
||||
<div className="py-8" />
|
||||
)
|
||||
}>
|
||||
<ElementsSeparator className="max-w-2xl">
|
||||
{[
|
||||
<div key="thumbnailHeader" className="grid place-items-center gap-6">
|
||||
<ThumbnailHeader
|
||||
className="max-w-2xl"
|
||||
thumbnail={content.thumbnail?.data?.attributes}
|
||||
pre_title={selectedTranslation?.pre_title}
|
||||
title={selectedTranslation?.title}
|
||||
subtitle={selectedTranslation?.subtitle}
|
||||
description={selectedTranslation?.description}
|
||||
categories={filterHasAttributes(content.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
)}
|
||||
type={
|
||||
content.type?.data?.attributes
|
||||
? formatContentType(content.type.data.attributes.slug)
|
||||
: undefined
|
||||
}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{setTypeSelectorProps.filter((button) => button.visible).length > 1 && (
|
||||
<ButtonGroup buttonsProps={setTypeSelectorProps} />
|
||||
)}
|
||||
</div>,
|
||||
<ThumbnailHeader
|
||||
key="thumbnailHeader"
|
||||
className="max-w-2xl"
|
||||
thumbnail={content.thumbnail?.data?.attributes}
|
||||
pre_title={selectedTranslation?.pre_title}
|
||||
title={selectedTranslation?.title}
|
||||
subtitle={selectedTranslation?.subtitle}
|
||||
description={selectedTranslation?.description}
|
||||
type={content.type}
|
||||
categories={content.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>,
|
||||
|
||||
content.previous_contents?.data && content.previous_contents.data.length > 0 && (
|
||||
<RelatedContentsSection
|
||||
|
@ -275,36 +283,8 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|||
/>
|
||||
),
|
||||
|
||||
selectedSetType === "text_set" && selectedTranslation?.text_set?.text ? (
|
||||
selectedTranslation?.text_set?.text && (
|
||||
<Markdawn className="max-w-2xl" text={selectedTranslation.text_set.text} />
|
||||
) : selectedSetType === "audio_set" &&
|
||||
selectedTranslation?.audio_set &&
|
||||
selectedTranslation.language?.data?.attributes?.code ? (
|
||||
<AudioPlayer
|
||||
title={prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)}
|
||||
src={getAudioURL(content.slug, selectedTranslation.language.data.attributes.code)}
|
||||
className="max-w-2xl"
|
||||
/>
|
||||
) : (
|
||||
selectedSetType === "video_set" &&
|
||||
selectedTranslation?.video_set &&
|
||||
selectedTranslation.language?.data?.attributes?.code && (
|
||||
<VideoPlayer
|
||||
title={prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)}
|
||||
src={getVideoURL(content.slug, selectedTranslation.language.data.attributes.code)}
|
||||
subSrc={`${process.env.NEXT_PUBLIC_URL_ASSETS}/contents/videos/\
|
||||
${content.slug}_${selectedTranslation.language.data.attributes.code}.vtt`}
|
||||
className="max-w-[90vh]"
|
||||
/>
|
||||
)
|
||||
),
|
||||
|
||||
content.next_contents?.data && content.next_contents.data.length > 0 && (
|
||||
|
@ -332,15 +312,18 @@ export default Content;
|
|||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format, formatCategory, formatContentType } = getFormat(context.locale);
|
||||
const { format } = getFormat(context.locale);
|
||||
const slug = context.params?.slug ? context.params.slug.toString() : "";
|
||||
const content = await sdk.getContentText({ slug: slug });
|
||||
const content = await sdk.getContentText({
|
||||
slug: slug,
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
|
||||
if (!content.contents?.data[0]?.attributes?.translations) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
const { title, description, audio, video } = (() => {
|
||||
const { title, description } = (() => {
|
||||
if (context.locale && context.locales) {
|
||||
const selectedTranslation = staticSmartLanguage({
|
||||
items: content.contents.data[0].attributes.translations,
|
||||
|
@ -348,43 +331,27 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
||||
});
|
||||
if (selectedTranslation) {
|
||||
const rawDescription = isDefinedAndNotEmpty(selectedTranslation.description)
|
||||
? selectedTranslation.description
|
||||
: selectedTranslation.text_set?.text;
|
||||
|
||||
return {
|
||||
title: prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
),
|
||||
description: getDescription(rawDescription, {
|
||||
description: getDescription(selectedTranslation.description, {
|
||||
[format("type", { count: Infinity })]: [
|
||||
content.contents.data[0].attributes.type?.data?.attributes
|
||||
? formatContentType(content.contents.data[0].attributes.type.data.attributes.slug)
|
||||
: undefined,
|
||||
content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title,
|
||||
],
|
||||
[format("category", { count: Infinity })]: filterHasAttributes(
|
||||
content.contents.data[0].attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => formatCategory(category.attributes.slug)),
|
||||
).map((category) => category.attributes.short),
|
||||
}),
|
||||
audio:
|
||||
selectedTranslation.language?.data?.attributes?.code && selectedTranslation.audio_set
|
||||
? getAudioURL(slug, selectedTranslation.language.data.attributes.code)
|
||||
: undefined,
|
||||
video:
|
||||
selectedTranslation.language?.data?.attributes?.code && selectedTranslation.video_set
|
||||
? getVideoURL(slug, selectedTranslation.language.data.attributes.code)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: prettySlug(content.contents.data[0].attributes.slug),
|
||||
description: undefined,
|
||||
audio: undefined,
|
||||
video: undefined,
|
||||
};
|
||||
})();
|
||||
|
||||
|
@ -392,7 +359,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
|
||||
const props: Props = {
|
||||
content: content.contents.data[0].attributes as ContentWithTranslations,
|
||||
openGraph: getOpenGraph(format, title, description, thumbnail, audio, video),
|
||||
openGraph: getOpenGraph(format, title, description, thumbnail),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
|
@ -442,7 +409,6 @@ const RelatedContentsSection = ({
|
|||
open={isOpened}
|
||||
onClosing={() => setOpened(false)}
|
||||
onOpening={() => setOpened(true)}
|
||||
overflowWhenOpen="visible"
|
||||
trigger={
|
||||
<div className="flex place-content-center place-items-center gap-4">
|
||||
<h2 className="text-center text-2xl">{title}</h2>
|
||||
|
@ -450,7 +416,7 @@ const RelatedContentsSection = ({
|
|||
</div>
|
||||
}
|
||||
contentInnerClassName={cJoin(
|
||||
cIf(contents.length > 1, "py-10", "py-6"),
|
||||
cIf(contents.length > 1, "px-4 py-10", "px-4 py-6"),
|
||||
"flex w-full flex-wrap place-content-center items-start gap-x-6 gap-y-8"
|
||||
)}
|
||||
easing="ease-in-out"
|
||||
|
@ -471,7 +437,6 @@ const RelatedContentPreview = ({
|
|||
categories,
|
||||
type,
|
||||
}: RelatedContentPreviewFragment) => {
|
||||
const { formatCategory, formatContentType } = useFormat();
|
||||
const isContentPanelAtLeastXl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastXl);
|
||||
|
||||
return (
|
||||
|
@ -488,24 +453,17 @@ const RelatedContentPreview = ({
|
|||
)}
|
||||
fallback={{ title: slug }}
|
||||
thumbnail={thumbnail?.data?.attributes}
|
||||
topChips={type?.data?.attributes ? [formatContentType(type.data.attributes.slug)] : undefined}
|
||||
bottomChips={filterHasAttributes(categories?.data, ["attributes"]).map((category) =>
|
||||
formatCategory(category.attributes.slug)
|
||||
)}
|
||||
topChips={
|
||||
type?.data?.attributes
|
||||
? [
|
||||
type.data.attributes.titles?.[0]
|
||||
? type.data.attributes.titles[0]?.title
|
||||
: prettySlug(type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={categories?.data.map((category) => category.attributes?.short ?? "")}
|
||||
keepInfoVisible
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭───────────────────╮
|
||||
* ─────────────────────────────────────╯ PRIVATE METHODS ╰───────────────────────────────────────
|
||||
*/
|
||||
|
||||
const getAudioURL = (slug: string, langCode: string): string =>
|
||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/contents/audios/\
|
||||
${slug}_${langCode}.mp3`;
|
||||
|
||||
const getVideoURL = (slug: string, langCode: string): string =>
|
||||
`${process.env.NEXT_PUBLIC_URL_ASSETS}/contents/videos/\
|
||||
${slug}_${langCode}.mp4`;
|
||||
|
|
|
@ -29,7 +29,7 @@ import { prettySlug } from "helpers/formatters";
|
|||
import { Paginator } from "components/Containers/Paginator";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
|
@ -42,14 +42,12 @@ const DEFAULT_FILTERS_STATE = {
|
|||
keepInfoVisible: true,
|
||||
query: "",
|
||||
page: 1,
|
||||
lang: 0,
|
||||
};
|
||||
|
||||
const queryParamSchema = z.object({
|
||||
query: z.coerce.string().optional(),
|
||||
page: z.coerce.number().positive().optional(),
|
||||
sort: z.coerce.number().min(0).max(5).optional(),
|
||||
lang: z.coerce.number().min(0).optional(),
|
||||
});
|
||||
|
||||
/*
|
||||
|
@ -61,10 +59,9 @@ interface Props extends AppLayoutRequired {}
|
|||
|
||||
const Contents = (props: Props): JSX.Element => {
|
||||
const hoverable = useDeviceSupportsHover();
|
||||
const { format, formatCategory, formatContentType, formatLanguage } = useFormat();
|
||||
const { format } = useFormat();
|
||||
const router = useTypedRouter(queryParamSchema);
|
||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const sortingMethods = useMemo(
|
||||
() => [
|
||||
|
@ -75,17 +72,6 @@ const Contents = (props: Props): JSX.Element => {
|
|||
[format]
|
||||
);
|
||||
|
||||
const languageOptions = useMemo(() => {
|
||||
const memo =
|
||||
router.locales?.map((language) => ({
|
||||
meiliAttribute: language,
|
||||
displayedName: formatLanguage(language),
|
||||
})) ?? [];
|
||||
|
||||
memo.unshift({ meiliAttribute: "", displayedName: format("all") });
|
||||
return memo;
|
||||
}, [router.locales, formatLanguage, format]);
|
||||
|
||||
const [sortingMethod, setSortingMethod] = useState<number>(
|
||||
router.query.sort ?? DEFAULT_FILTERS_STATE.sortingMethod
|
||||
);
|
||||
|
@ -96,41 +82,25 @@ const Contents = (props: Props): JSX.Element => {
|
|||
setValue: setKeepInfoVisible,
|
||||
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
|
||||
const [contents, setContents] = useState<CustomSearchResponse<MeiliContent>>();
|
||||
const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page);
|
||||
const [contents, setContents] = useState<CustomSearchResponse<MeiliContent>>();
|
||||
const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query);
|
||||
const [languageOption, setLanguageOption] = useState(
|
||||
router.query.lang ?? DEFAULT_FILTERS_STATE.lang
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
const currentSortingMethod = sortingMethods[sortingMethod]?.meiliAttribute;
|
||||
const currentLanguageOption = languageOptions[languageOption]?.meiliAttribute;
|
||||
|
||||
const filter: string[] = [];
|
||||
if (languageOption !== 0) {
|
||||
filter.push(`filterable_languages = ${currentLanguageOption}`);
|
||||
}
|
||||
|
||||
const currentSortingMethod = sortingMethods[sortingMethod];
|
||||
const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, {
|
||||
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
|
||||
attributesToHighlight: ["translations"],
|
||||
attributesToCrop: ["translations.displayable_description"],
|
||||
filter,
|
||||
hitsPerPage: 25,
|
||||
page,
|
||||
sort: isDefined(currentSortingMethod) ? [currentSortingMethod] : undefined,
|
||||
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
|
||||
});
|
||||
|
||||
setContents(
|
||||
languageOption === 0
|
||||
? filterHitsWithHighlight<MeiliContent>(searchResult, "translations")
|
||||
: searchResult
|
||||
);
|
||||
setContents(filterHitsWithHighlight<MeiliContent>(searchResult, "translations"));
|
||||
};
|
||||
fetchPosts();
|
||||
}, [query, page, sortingMethod, sortingMethods, languageOption, languageOptions]);
|
||||
}, [query, page, sortingMethod, sortingMethods]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady)
|
||||
|
@ -138,37 +108,19 @@ const Contents = (props: Props): JSX.Element => {
|
|||
page,
|
||||
query,
|
||||
sort: sortingMethod,
|
||||
lang: languageOption,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, query, languageOption, sortingMethod, router.isReady]);
|
||||
}, [page, query, sortingMethod, router.isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
if (isDefined(router.query.page)) setPage(router.query.page);
|
||||
if (isDefined(router.query.query)) setQuery(router.query.query);
|
||||
if (isDefined(router.query.sort)) setSortingMethod(router.query.sort);
|
||||
if (isDefined(router.query.lang)) setLanguageOption(router.query.lang);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady]);
|
||||
|
||||
const searchInput = (
|
||||
<TextInput
|
||||
placeholder={format("search_placeholder")}
|
||||
value={query}
|
||||
onChange={(name) => {
|
||||
setPage(1);
|
||||
setQuery(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Contents/All", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Contents/All", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
|
@ -188,7 +140,20 @@ const Contents = (props: Props): JSX.Element => {
|
|||
|
||||
<HorizontalLine />
|
||||
|
||||
{!is1ColumnLayout && <div className="mb-6">{searchInput}</div>}
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={format("search_title")}
|
||||
value={query}
|
||||
onChange={(name) => {
|
||||
setPage(1);
|
||||
setQuery(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Contents/All", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Contents/All", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={format("order_by")}>
|
||||
<Select
|
||||
|
@ -208,24 +173,6 @@ const Contents = (props: Props): JSX.Element => {
|
|||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={format("language", { count: Infinity })}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={languageOptions.map((item) => item.displayedName)}
|
||||
value={languageOption}
|
||||
onChange={(newLanguageOption) => {
|
||||
setPage(1);
|
||||
setLanguageOption(newLanguageOption);
|
||||
sendAnalytics(
|
||||
"Contents/All",
|
||||
`Change language filter (${
|
||||
languageOptions.map((item) => item.meiliAttribute)[newLanguageOption]
|
||||
})`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={format("always_show_info")}>
|
||||
<Switch
|
||||
|
@ -243,11 +190,10 @@ const Contents = (props: Props): JSX.Element => {
|
|||
text={format("reset_all_filters")}
|
||||
icon="settings_backup_restore"
|
||||
onClick={() => {
|
||||
setPage(DEFAULT_FILTERS_STATE.page);
|
||||
setPage(1);
|
||||
setQuery(DEFAULT_FILTERS_STATE.query);
|
||||
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
setLanguageOption(DEFAULT_FILTERS_STATE.lang);
|
||||
sendAnalytics("Contents/All", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
|
@ -256,7 +202,6 @@ const Contents = (props: Props): JSX.Element => {
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{is1ColumnLayout && <div className="mx-auto mb-12 max-w-lg">{searchInput}</div>}
|
||||
<Paginator page={page} onPageChange={setPage} totalNumberOfPages={contents?.totalPages}>
|
||||
<div
|
||||
className="grid grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-start
|
||||
|
@ -267,30 +212,28 @@ const Contents = (props: Props): JSX.Element => {
|
|||
href={`/contents/${item.slug}`}
|
||||
translations={filterHasAttributes(item._formatted.translations, [
|
||||
"language.data.attributes.code",
|
||||
])
|
||||
.map(({ displayable_description, language, ...otherAttributes }) => ({
|
||||
...otherAttributes,
|
||||
description: containsHighlight(displayable_description)
|
||||
? displayable_description
|
||||
: undefined,
|
||||
language: language.data.attributes.code,
|
||||
}))
|
||||
.filter(
|
||||
({ language }) =>
|
||||
languageOption === 0 ||
|
||||
language === languageOptions[languageOption]?.meiliAttribute
|
||||
)}
|
||||
]).map(({ displayable_description, language, ...otherAttributes }) => ({
|
||||
...otherAttributes,
|
||||
description: containsHighlight(displayable_description)
|
||||
? displayable_description
|
||||
: undefined,
|
||||
language: language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.slug) }}
|
||||
thumbnail={item.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.type?.data?.attributes
|
||||
? [formatContentType(item.type.data.attributes.slug)]
|
||||
? [
|
||||
item.type.data.attributes.titles?.[0]
|
||||
? item.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
bottomChips={item.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
|
|
|
@ -4,10 +4,11 @@ import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/Cont
|
|||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { ParentFolderPreviewFragment, UploadImageFragment } from "graphql/generated";
|
||||
import { GetContentsFolderQuery } from "graphql/generated";
|
||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { Ico } from "components/Ico";
|
||||
import { Button, TranslatedButton } from "components/Inputs/Button";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
import { SubPanel } from "components/Containers/SubPanel";
|
||||
import { TranslatedPreviewCard } from "components/PreviewCard";
|
||||
|
@ -19,7 +20,6 @@ import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
|
|||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { Chip } from "components/Chip";
|
||||
import { FolderPath } from "components/Contents/FolderPath";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -27,31 +27,12 @@ import { FolderPath } from "components/Contents/FolderPath";
|
|||
*/
|
||||
|
||||
interface Props extends AppLayoutRequired {
|
||||
subfolders: {
|
||||
slug: string;
|
||||
href: string;
|
||||
translations: { language: string; title: string }[];
|
||||
fallback: { title: string };
|
||||
}[];
|
||||
contents: {
|
||||
slug: string;
|
||||
href: string;
|
||||
translations: { language: string; pre_title?: string; title: string; subtitle?: string }[];
|
||||
fallback: { title: string };
|
||||
thumbnail?: UploadImageFragment;
|
||||
topChips?: string[];
|
||||
bottomChips?: string[];
|
||||
}[];
|
||||
path: ParentFolderPreviewFragment[];
|
||||
folder: NonNullable<
|
||||
NonNullable<GetContentsFolderQuery["contentsFolders"]>["data"][number]["attributes"]
|
||||
>;
|
||||
}
|
||||
|
||||
const ContentsFolder = ({
|
||||
openGraph,
|
||||
path,
|
||||
contents,
|
||||
subfolders,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
|
||||
|
@ -77,51 +58,122 @@ const ContentsFolder = ({
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<FolderPath path={path} />
|
||||
<div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
|
||||
{folder.parent_folder?.data?.attributes && (
|
||||
<>
|
||||
{folder.parent_folder.data.attributes.slug === "root" ? (
|
||||
<Button href="/contents" icon="home" />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
|
||||
translations={filterHasAttributes(folder.parent_folder.data.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
]).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.parent_folder.data.attributes.slug),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Ico icon="chevron_right" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{subfolders.length > 0 && (
|
||||
{folder.slug === "root" ? (
|
||||
<Button href="/contents" icon="home" active />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
translations={filterHasAttributes(folder.titles, ["language.data.attributes.code"]).map(
|
||||
(title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
})
|
||||
)}
|
||||
fallback={{
|
||||
text: prettySlug(folder.slug),
|
||||
}}
|
||||
active
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{folder.subfolders?.data && folder.subfolders.data.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-2 flex place-items-center gap-2">
|
||||
<h2 className="text-2xl">{format("folders")}</h2>
|
||||
<Chip text={format("x_results", { x: subfolders.length })} />
|
||||
</div>
|
||||
<h2 className="flex flex-row place-items-center gap-2 pb-2 text-2xl">
|
||||
{format("folders")}
|
||||
<Chip text={format("x_results", { x: folder.subfolders.data.length })} />
|
||||
</h2>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid items-start pb-12",
|
||||
"grid items-start gap-8 pb-12",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}>
|
||||
{subfolders.map((subfolder) => (
|
||||
<TranslatedPreviewFolder key={subfolder.slug} {...subfolder} />
|
||||
{filterHasAttributes(folder.subfolders.data, ["id", "attributes"]).map((subfolder) => (
|
||||
<TranslatedPreviewFolder
|
||||
key={subfolder.id}
|
||||
href={`/contents/folder/${subfolder.attributes.slug}`}
|
||||
translations={filterHasAttributes(subfolder.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
]).map((title) => ({
|
||||
title: title.title,
|
||||
language: title.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(subfolder.attributes.slug) }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contents.length > 0 && (
|
||||
{folder.contents?.data && folder.contents.data.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-2 flex place-items-center gap-2">
|
||||
<h2 className="text-2xl">{format("contents")}</h2>
|
||||
<Chip text={format("x_results", { x: contents.length })} />
|
||||
</div>
|
||||
<h2 className="flex flex-row place-items-center gap-2 pb-2 text-2xl">
|
||||
{format("contents")}
|
||||
<Chip text={format("x_results", { x: folder.contents.data.length })} />
|
||||
</h2>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid items-start pb-12",
|
||||
"grid items-start gap-8 pb-12",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}>
|
||||
{contents.map((item) => (
|
||||
{filterHasAttributes(folder.contents.data, ["id", "attributes"]).map((item) => (
|
||||
<TranslatedPreviewCard
|
||||
key={item.slug}
|
||||
{...item}
|
||||
key={item.id}
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
]).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
))}
|
||||
|
@ -129,7 +181,9 @@ const ContentsFolder = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{contents.length === 0 && subfolders.length === 0 && <NoContentNorFolderMessage />}
|
||||
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
|
||||
<NoContentNorFolderMessage />
|
||||
)}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
|
@ -151,9 +205,12 @@ export default ContentsFolder;
|
|||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format, formatContentType, formatCategory } = getFormat(context.locale);
|
||||
const { format } = getFormat(context.locale);
|
||||
const slug = context.params?.slug ? context.params.slug.toString() : "";
|
||||
const contentsFolder = await sdk.getContentsFolder({ slug: slug });
|
||||
const contentsFolder = await sdk.getContentsFolder({
|
||||
slug: slug,
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!contentsFolder.contentsFolders?.data[0]?.attributes) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
@ -177,51 +234,12 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
return prettySlug(folder.slug);
|
||||
})();
|
||||
|
||||
const subfolders: Props["subfolders"] = filterHasAttributes(folder.subfolders?.data, [
|
||||
"attributes",
|
||||
]).map(({ attributes }) => ({
|
||||
slug: attributes.slug,
|
||||
href: `/contents/folder/${attributes.slug}`,
|
||||
translations: filterHasAttributes(attributes.titles, ["language.data.attributes.code"]).map(
|
||||
(translation) => ({
|
||||
title: translation.title,
|
||||
language: translation.language.data.attributes.code,
|
||||
})
|
||||
),
|
||||
fallback: { title: prettySlug(attributes.slug) },
|
||||
}));
|
||||
|
||||
const contents: Props["contents"] = filterHasAttributes(folder.contents?.data, [
|
||||
"attributes",
|
||||
]).map(({ attributes }) => ({
|
||||
slug: attributes.slug,
|
||||
href: `/contents/${attributes.slug}`,
|
||||
translations: filterHasAttributes(attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
]).map((translation) => ({
|
||||
pre_title: translation.pre_title ?? undefined,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle ?? undefined,
|
||||
language: translation.language.data.attributes.code,
|
||||
})),
|
||||
fallback: { title: prettySlug(attributes.slug) },
|
||||
thumbnail: attributes.thumbnail?.data?.attributes ?? undefined,
|
||||
topChips: attributes.type?.data?.attributes
|
||||
? [formatContentType(attributes.type.data.attributes.slug)]
|
||||
: undefined,
|
||||
bottomChips: filterHasAttributes(attributes.categories?.data, ["attributes"]).map((category) =>
|
||||
formatCategory(category.attributes.slug)
|
||||
),
|
||||
}));
|
||||
|
||||
const props: Props = {
|
||||
openGraph: getOpenGraph(format, title),
|
||||
subfolders,
|
||||
contents,
|
||||
path: getRecursiveParentFolderPreview(folder),
|
||||
folder,
|
||||
};
|
||||
return {
|
||||
props: JSON.parse(JSON.stringify(props)),
|
||||
props: props,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -255,32 +273,10 @@ const NoContentNorFolderMessage = () => {
|
|||
return (
|
||||
<div className="grid place-content-center">
|
||||
<div
|
||||
className="mt-12 grid grid-flow-col place-items-center gap-9 rounded-2xl border-2
|
||||
border-dotted border-dark p-8 text-dark opacity-40">
|
||||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
|
||||
border-dark p-8 text-dark opacity-40">
|
||||
<p className="max-w-xs text-2xl">{format("empty_folder_message")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭───────────────────╮
|
||||
* ─────────────────────────────────────╯ PRIVATE METHODS ╰───────────────────────────────────────
|
||||
*/
|
||||
|
||||
type ParentFolderWithParentFolder = ParentFolderPreviewFragment & {
|
||||
parent_folder?: {
|
||||
data?: {
|
||||
attributes?: ParentFolderPreviewFragment | ParentFolderWithParentFolder | null;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const getRecursiveParentFolderPreview = (
|
||||
parentFolder: ParentFolderWithParentFolder
|
||||
): ParentFolderPreviewFragment[] => [
|
||||
...(parentFolder.parent_folder?.data?.attributes
|
||||
? getRecursiveParentFolderPreview(parentFolder.parent_folder.data.attributes)
|
||||
: []),
|
||||
{ slug: parentFolder.slug, titles: parentFolder.titles },
|
||||
];
|
||||
|
|
|
@ -329,14 +329,6 @@ const Editor = (props: Props): JSX.Element => {
|
|||
<Button onClick={() => toggleWrap("`")} icon="code" />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Angelic/Celestial</h3>}>
|
||||
<Button
|
||||
onClick={() => toggleWrap("Angelic", {})}
|
||||
text="m"
|
||||
className="w-14 font-angelic font-bold"
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
|
@ -452,27 +444,6 @@ const Editor = (props: Props): JSX.Element => {
|
|||
<Button icon="dashboard" />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Videos</h3>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
onClick={() => insert(`<Video id="XXXXXXXXX" />`)}
|
||||
icon="movie"
|
||||
text="Video"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => insert(`<Video id="XXXXXXXXX" title="My Title" />`)}
|
||||
icon="movie"
|
||||
text="Video + title"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}>
|
||||
<Button icon="movie" />
|
||||
</ToolTip>
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Scene break</h3>}>
|
||||
<Button onClick={() => insert("\n* * *\n")} icon="more_horiz" />
|
||||
</ToolTip>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
|||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
import { TextInput } from "components/Inputs/TextInput";
|
||||
|
@ -17,7 +17,6 @@ import { PreviewCard } from "components/PreviewCard";
|
|||
import { ChroniclePreview } from "components/Chronicles/ChroniclePreview";
|
||||
import { PreviewFolder } from "components/Contents/PreviewFolder";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { AudioPlayer, VideoPlayer } from "components/Player";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -33,7 +32,6 @@ const DesignSystem = (props: Props): JSX.Element => {
|
|||
const [textInputState, setTextInputState] = useState("");
|
||||
const [textAreaState, setTextAreaState] = useState("");
|
||||
const [buttonGroupState, setButtonGroupState] = useState(0);
|
||||
const [verticalButtonGroupState, setVerticalButtonGroupState] = useState(0);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel
|
||||
|
@ -192,7 +190,7 @@ const DesignSystem = (props: Props): JSX.Element => {
|
|||
</div>
|
||||
|
||||
<HorizontalLine />
|
||||
<h3 className="-mt-6 text-xl">Small sized</h3>
|
||||
<h3 className="text-xl">Small sized</h3>
|
||||
|
||||
<div className="grid grid-cols-[repeat(4,auto)] place-content-center gap-4">
|
||||
<p className="self-center justify-self-start">Normal</p>
|
||||
|
@ -217,22 +215,12 @@ const DesignSystem = (props: Props): JSX.Element => {
|
|||
</div>
|
||||
|
||||
<HorizontalLine />
|
||||
<h3 className="-mt-6 text-xl">Groups</h3>
|
||||
<div className="grid grid-cols-2 place-items-center gap-4">
|
||||
<p>Normal sized</p>
|
||||
<p>Small sized</p>
|
||||
<h3 className="text-xl">Groups</h3>
|
||||
<div className="grid place-items-center gap-4">
|
||||
<ButtonGroup buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]} />
|
||||
<ButtonGroup
|
||||
buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]}
|
||||
size="small"
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||
size="small"
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ icon: "car_crash" },
|
||||
|
@ -241,40 +229,6 @@ const DesignSystem = (props: Props): JSX.Element => {
|
|||
{ icon: "cable" },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ icon: "car_crash" },
|
||||
{ icon: "timelapse", text: "Label", active: true },
|
||||
{ text: "Another Label" },
|
||||
{ icon: "cable" },
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
text: "Try me!",
|
||||
active: buttonGroupState === 0,
|
||||
onClick: () => setButtonGroupState(0),
|
||||
},
|
||||
{
|
||||
icon: "ad_units",
|
||||
text: "Label",
|
||||
active: buttonGroupState === 1,
|
||||
onClick: () => setButtonGroupState(1),
|
||||
},
|
||||
{
|
||||
text: "Yet another label",
|
||||
active: buttonGroupState === 2,
|
||||
onClick: () => setButtonGroupState(2),
|
||||
},
|
||||
{
|
||||
icon: "security",
|
||||
active: buttonGroupState === 3,
|
||||
onClick: () => setButtonGroupState(3),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
|
@ -299,102 +253,6 @@ const DesignSystem = (props: Props): JSX.Element => {
|
|||
onClick: () => setButtonGroupState(3),
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<h3 className="-mt-6 text-xl">Vertical groups</h3>
|
||||
<div className="grid grid-cols-2 place-items-center gap-4">
|
||||
<p>Normal sized</p>
|
||||
<p>Small sized</p>
|
||||
<ButtonGroup buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]} vertical />
|
||||
<ButtonGroup
|
||||
buttonsProps={[{ icon: "call_end" }, { icon: "zoom_in_map" }]}
|
||||
vertical
|
||||
size="small"
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||
vertical
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[{ icon: "car_crash" }, { icon: "timelapse" }, { icon: "leak_add" }]}
|
||||
vertical
|
||||
size="small"
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ icon: "car_crash" },
|
||||
{ icon: "timelapse", text: "Label", active: true },
|
||||
{ text: "Another Label" },
|
||||
{ icon: "cable" },
|
||||
]}
|
||||
vertical
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ icon: "car_crash" },
|
||||
{ icon: "timelapse", text: "Label", active: true },
|
||||
{ text: "Another Label" },
|
||||
{ icon: "cable" },
|
||||
]}
|
||||
vertical
|
||||
size="small"
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
text: "Try me!",
|
||||
active: verticalButtonGroupState === 0,
|
||||
onClick: () => setVerticalButtonGroupState(0),
|
||||
},
|
||||
{
|
||||
icon: "ad_units",
|
||||
text: "Label",
|
||||
active: verticalButtonGroupState === 1,
|
||||
onClick: () => setVerticalButtonGroupState(1),
|
||||
},
|
||||
{
|
||||
text: "Yet another label",
|
||||
active: verticalButtonGroupState === 2,
|
||||
onClick: () => setVerticalButtonGroupState(2),
|
||||
},
|
||||
{
|
||||
icon: "security",
|
||||
active: verticalButtonGroupState === 3,
|
||||
onClick: () => setVerticalButtonGroupState(3),
|
||||
},
|
||||
]}
|
||||
vertical
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
text: "Try me!",
|
||||
active: verticalButtonGroupState === 0,
|
||||
onClick: () => setVerticalButtonGroupState(0),
|
||||
},
|
||||
{
|
||||
icon: "ad_units",
|
||||
text: "Label",
|
||||
active: verticalButtonGroupState === 1,
|
||||
onClick: () => setVerticalButtonGroupState(1),
|
||||
},
|
||||
{
|
||||
text: "Yet another label",
|
||||
active: verticalButtonGroupState === 2,
|
||||
onClick: () => setVerticalButtonGroupState(2),
|
||||
},
|
||||
{
|
||||
icon: "security",
|
||||
active: verticalButtonGroupState === 3,
|
||||
onClick: () => setVerticalButtonGroupState(3),
|
||||
},
|
||||
]}
|
||||
vertical
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</TwoThemedSection>
|
||||
|
@ -956,28 +814,6 @@ const DesignSystem = (props: Props): JSX.Element => {
|
|||
<PreviewFolder href="#" title="Disabled, with a longer title" disabled />
|
||||
</div>
|
||||
</TwoThemedSection>
|
||||
|
||||
<TwoThemedSection className="grid gap-4" fullWidth>
|
||||
<h3 className="mb-2 text-xl">Audio players</h3>
|
||||
|
||||
<AudioPlayer src="https://resha.re/public-domain/Prelude-No.15-in-G-major-BWV-860.mp3" />
|
||||
<AudioPlayer
|
||||
title="A longer audio track, with a title"
|
||||
src="https://resha.re/public-domain/Muriel-Nguyen-Xuan-Brahms-rhapsody-opus79-1.ogg"
|
||||
/>
|
||||
<AudioPlayer
|
||||
title={`The same audio tack, but this time, an obnoxiously long title that frankly at\
|
||||
this point should stop because who in their right mind would read that much text for a title.`}
|
||||
src="https://resha.re/public-domain/Muriel-Nguyen-Xuan-Brahms-rhapsody-opus79-1.ogg"
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<h3 className="mb-2 text-xl">Video players</h3>
|
||||
<VideoPlayer src={`https://resha.re/public-domain/the_whistler_1944.mp4`} />
|
||||
<VideoPlayer
|
||||
src={`https://resha.re/public-domain/big_buck_bunny_720p_surround.mp4`}
|
||||
title="Big Buck Bunny - Blender Foundation"
|
||||
/>
|
||||
</TwoThemedSection>
|
||||
</ContentPanel>
|
||||
);
|
||||
return <AppLayout {...props} contentPanel={contentPanel} />;
|
||||
|
@ -1008,15 +844,10 @@ export const getStaticProps: GetStaticProps = (context) => {
|
|||
interface ThemedSectionProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const TwoThemedSection = ({ children, className, fullWidth }: ThemedSectionProps) => (
|
||||
<div
|
||||
className={cJoin(
|
||||
"mb-12 grid grid-flow-col drop-shadow-lg shadow-shade",
|
||||
cIf(fullWidth, "w-full")
|
||||
)}>
|
||||
const TwoThemedSection = ({ children, className }: ThemedSectionProps) => (
|
||||
<div className="mb-12 grid grid-flow-col drop-shadow-lg shadow-shade">
|
||||
<LightThemeSection className={cJoin("rounded-l-xl text-black", className)}>
|
||||
{children}
|
||||
</LightThemeSection>
|
||||
|
|
|
@ -18,14 +18,9 @@ import { cIf, cJoin } from "helpers/className";
|
|||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
type Orientation = "horizontal" | "vertical";
|
||||
|
||||
const textAtom = atomPairing(atomWithStorage("transcriptText", ""));
|
||||
const fontSizeAtom = atomPairing(atomWithStorage("transcriptFontSize", 1));
|
||||
const textOffsetAtom = atomPairing(atomWithStorage("transcriptTextOffset", 0));
|
||||
const orientationAtom = atomPairing(
|
||||
atomWithStorage<Orientation>("transcriptOrientation", "vertical")
|
||||
);
|
||||
const textOffset = atomPairing(atomWithStorage("transcriptTextOffset", 0));
|
||||
|
||||
const SIZE_MULTIPLIER = 1000;
|
||||
|
||||
|
@ -55,8 +50,7 @@ const swapChar = (char: string, swaps: string[]): string => {
|
|||
const Transcript = (props: Props): JSX.Element => {
|
||||
const [text, setText] = useAtomPair(textAtom);
|
||||
const [fontSize, setFontSize] = useAtomPair(fontSizeAtom);
|
||||
const [offset, setOffset] = useAtomPair(textOffsetAtom);
|
||||
const [orientation, setOrientation] = useAtomPair(orientationAtom);
|
||||
const [xOffset, setXOffset] = useAtomPair(textOffset);
|
||||
const [lineIndex, setLineIndex] = useState(0);
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
@ -415,32 +409,52 @@ const Transcript = (props: Props): JSX.Element => {
|
|||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full} className="!pt-2">
|
||||
<div className="flex flex-wrap items-end gap-4 pr-24">
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
active: orientation === "horizontal",
|
||||
icon: "text_rotation_none",
|
||||
onClick: () => setOrientation("horizontal"),
|
||||
},
|
||||
{
|
||||
active: orientation === "vertical",
|
||||
icon: "text_rotate_vertical",
|
||||
onClick: () => setOrientation("vertical"),
|
||||
},
|
||||
]}
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full} className="!pr-0 !pt-0">
|
||||
<div
|
||||
className={cJoin("grid", cIf(image, "grid-cols-[1fr_5rem_20rem]", "grid-cols-[1fr_5rem]"))}>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onChange={updateDisplayedText}
|
||||
onClick={updateLineIndex}
|
||||
onKeyUp={updateLineIndex}
|
||||
title="Input textarea"
|
||||
className="mt-4 whitespace-pre"
|
||||
value={text}
|
||||
/>
|
||||
|
||||
<p
|
||||
className="z-10 mt-4 h-[80vh] whitespace-nowrap
|
||||
font-bold [font-family:Noto_Serif_JP] [transform-origin:top_right]
|
||||
[writing-mode:vertical-rl]"
|
||||
style={{
|
||||
transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`,
|
||||
}}>
|
||||
{text.split("\n")[lineIndex]}
|
||||
</p>
|
||||
|
||||
{image && (
|
||||
<TransformWrapper
|
||||
panning={{ velocityDisabled: true }}
|
||||
alignmentAnimation={{ disabled: true }}
|
||||
wheel={{ step: 0.05 }}
|
||||
limitToBounds={false}>
|
||||
<TransformComponent wrapperStyle={{ height: "95vh" }}>
|
||||
<img src={image} alt="This provided image" className="w-full object-cover" />
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap place-items-center gap-4 pr-24">
|
||||
<div className="grid place-items-center">
|
||||
<p>Text offset: {offset}px</p>
|
||||
<p>Text offset: {xOffset}px</p>
|
||||
<input
|
||||
title="Font size multiplier"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={offset * 5}
|
||||
onChange={(event) => setOffset(parseInt(event.target.value, 10) / 5)}
|
||||
value={xOffset * 5}
|
||||
onChange={(event) => setXOffset(parseInt(event.target.value, 10) / 5)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -572,56 +586,6 @@ const Transcript = (props: Props): JSX.Element => {
|
|||
|
||||
<input type="file" accept="image/png, image/jpeg, image/webp" onChange={onImageUploaded} />
|
||||
</div>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid h-[90vh]",
|
||||
cIf(
|
||||
orientation === "vertical",
|
||||
cIf(image, "grid-cols-[1fr_5rem_20rem]", "grid-cols-[1fr_5rem]"),
|
||||
cIf(image, "grid-rows-[1fr_5rem_20rem]", "grid-rows-[1fr_5rem]")
|
||||
)
|
||||
)}>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onChange={updateDisplayedText}
|
||||
onClick={updateLineIndex}
|
||||
onKeyUp={updateLineIndex}
|
||||
title="Input textarea"
|
||||
className="mt-4 whitespace-pre"
|
||||
value={text}
|
||||
/>
|
||||
|
||||
<p
|
||||
className={cJoin(
|
||||
`z-10 mt-4 whitespace-nowrap
|
||||
font-bold [font-family:Noto_Serif_JP]`,
|
||||
cIf(
|
||||
orientation === "vertical",
|
||||
"[transform-origin:top_right] [writing-mode:vertical-rl]",
|
||||
"[transform-origin:top_left]"
|
||||
)
|
||||
)}
|
||||
style={{
|
||||
transform: `scale(${fontSize}) ${
|
||||
orientation === "vertical" ? "translateX" : "translateY"
|
||||
}(${fontSize * offset}px)`,
|
||||
}}>
|
||||
{text.split("\n")[lineIndex]}
|
||||
</p>
|
||||
|
||||
{image && (
|
||||
<TransformWrapper
|
||||
panning={{ velocityDisabled: true }}
|
||||
alignmentAnimation={{ disabled: true }}
|
||||
wheel={{ step: 0.05 }}
|
||||
limitToBounds={false}
|
||||
minScale={0.5}>
|
||||
<TransformComponent wrapperStyle={{ height: "100%", width: "100%" }}>
|
||||
<img src={image} alt="The provided image" className="w-full object-cover" />
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ const Home = (props: PostStaticProps): JSX.Element => {
|
|||
[mask:url('/icons/accords.svg')]"
|
||||
/>
|
||||
<h1 className="mb-0 text-5xl">Accord’s Library</h1>
|
||||
<h2 className="-mt-5 font-angelic text-lg">Discover • Analyze • Translate • Archive</h2>
|
||||
<h2 className="-mt-5 text-xl">Discover • Analyze • Translate • Archive</h2>
|
||||
</div>
|
||||
}
|
||||
displayTitle={false}
|
||||
|
@ -64,7 +64,10 @@ export default Home;
|
|||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format } = getFormat(context.locale);
|
||||
const post = await sdk.getPost({ slug: "home" });
|
||||
const post = await sdk.getPost({
|
||||
slug: "home",
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (post.posts?.data && post.posts.data.length > 0) {
|
||||
const props: PostStaticProps = {
|
||||
post: post.posts.data[0]?.attributes as PostWithTranslations,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Chip } from "components/Chip";
|
||||
|
@ -19,7 +20,14 @@ import {
|
|||
GetLibraryItemQuery,
|
||||
} from "graphql/generated";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { prettyInlineTitle, prettySlug, prettyURL } from "helpers/formatters";
|
||||
import {
|
||||
prettyDate,
|
||||
prettyInlineTitle,
|
||||
prettyItemSubType,
|
||||
prettyPrice,
|
||||
prettySlug,
|
||||
prettyURL,
|
||||
} from "helpers/formatters";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { convertMmToInch } from "helpers/numbers";
|
||||
import { sortRangedContent } from "helpers/others";
|
||||
|
@ -29,10 +37,11 @@ import {
|
|||
isDefined,
|
||||
isDefinedAndNotEmpty,
|
||||
} from "helpers/asserts";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollOnChange";
|
||||
import { getScanArchiveURL, getTrackURL, isUntangibleGroupItem } from "helpers/libraryItem";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { WithLabel } from "components/Inputs/WithLabel";
|
||||
import { Ico } from "components/Ico";
|
||||
import { cJoin, cIf } from "helpers/className";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
|
@ -41,15 +50,10 @@ import { useIntersectionList } from "hooks/useIntersectionList";
|
|||
import { Ids } from "types/ids";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import {
|
||||
ElementsSeparator,
|
||||
FormatWithComponent,
|
||||
formatWithComponentSplitter,
|
||||
} from "helpers/component";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { AudioPlayer } from "components/Player";
|
||||
import { ElementsSeparator } from "helpers/component";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -66,38 +70,18 @@ const intersectionIds = ["summary", "gallery", "details", "subitems", "contents"
|
|||
interface Props extends AppLayoutRequired {
|
||||
item: NonNullable<NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["attributes"]>;
|
||||
itemId: NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["id"];
|
||||
tracks: { id: string; title: string; src: string }[];
|
||||
hasContentScans: boolean;
|
||||
isVariantSet: boolean;
|
||||
hasContentSection: boolean;
|
||||
}
|
||||
|
||||
const LibrarySlug = ({
|
||||
item,
|
||||
itemId,
|
||||
tracks,
|
||||
hasContentScans,
|
||||
isVariantSet,
|
||||
hasContentSection,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
||||
const currency = useAtomGetter(atoms.settings.currency);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const {
|
||||
format,
|
||||
formatLibraryItemType,
|
||||
formatCategory,
|
||||
formatContentType,
|
||||
formatLibraryItemSubType,
|
||||
formatPrice,
|
||||
formatDate,
|
||||
} = useFormat();
|
||||
const { format, formatLibraryItemType } = useFormat();
|
||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||
|
||||
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
|
||||
const isContentPanelAtLeastSm = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastSm);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const hoverable = useDeviceSupportsHover();
|
||||
const router = useRouter();
|
||||
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(false);
|
||||
|
||||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||
|
@ -107,12 +91,27 @@ const LibrarySlug = ({
|
|||
useScrollTopOnChange(Ids.ContentPanel, [itemId]);
|
||||
const currentIntersection = useIntersectionList(intersectionIds);
|
||||
|
||||
const isVariantSet =
|
||||
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
|
||||
|
||||
const displayOpenScans = item.contents?.data.some(
|
||||
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||
);
|
||||
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ElementsSeparator>
|
||||
{[
|
||||
!is1ColumnLayout && (
|
||||
<ReturnButton key="ReturnButton" href="/library/" title={format("library")} />
|
||||
is3ColumnsLayout && (
|
||||
<ReturnButton
|
||||
key="ReturnButton"
|
||||
href="/library/"
|
||||
title={format("library")}
|
||||
displayOnlyOn="3ColumnsLayout"
|
||||
/>
|
||||
),
|
||||
<div className="grid gap-4" key="NavOption">
|
||||
<NavOption
|
||||
|
@ -143,11 +142,7 @@ const LibrarySlug = ({
|
|||
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<NavOption
|
||||
title={
|
||||
isVariantSet
|
||||
? format("variant", { count: Infinity })
|
||||
: format("subitem", { count: Infinity })
|
||||
}
|
||||
title={format(isVariantSet ? "variant" : "subitem", { count: Infinity })}
|
||||
url={`#${intersectionIds[3]}`}
|
||||
border
|
||||
active={currentIntersection === 3}
|
||||
|
@ -155,7 +150,7 @@ const LibrarySlug = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{hasContentSection && (
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<NavOption
|
||||
title={format("contents")}
|
||||
url={`#${intersectionIds[4]}`}
|
||||
|
@ -172,15 +167,16 @@ const LibrarySlug = ({
|
|||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{is1ColumnLayout && (
|
||||
<ReturnButton href="/library/" title={format("library")} className="mb-10" />
|
||||
)}
|
||||
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={format("library")}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative h-[50vh] w-full cursor-pointer",
|
||||
cIf(!isPerfModeEnabled, "drop-shadow-xl shadow-shade"),
|
||||
"relative h-[50vh] w-full cursor-pointer drop-shadow-xl shadow-shade",
|
||||
cIf(isContentPanelAtLeast3xl, "mb-16", "h-[60vh]")
|
||||
)}>
|
||||
{item.thumbnail?.data?.attributes ? (
|
||||
|
@ -209,12 +205,11 @@ const LibrarySlug = ({
|
|||
item.subitem_of.data[0].attributes.title,
|
||||
item.subitem_of.data[0].attributes.subtitle
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid place-items-center text-center">
|
||||
<h1 className="text-4xl">{item.title}</h1>
|
||||
<h1 className="text-3xl">{item.title}</h1>
|
||||
{isDefinedAndNotEmpty(item.subtitle) && <h2 className="text-2xl">{item.subtitle}</h2>}
|
||||
</div>
|
||||
|
||||
|
@ -234,14 +229,12 @@ const LibrarySlug = ({
|
|||
<>
|
||||
{item.urls?.length ? (
|
||||
<div className="flex flex-row place-items-center gap-3">
|
||||
<FormatWithComponent
|
||||
text={format("available_at_x", { x: formatWithComponentSplitter })}
|
||||
component={filterHasAttributes(item.urls, ["url"]).map((url, index) => (
|
||||
<Fragment key={index}>
|
||||
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
|
||||
</Fragment>
|
||||
))}
|
||||
/>
|
||||
<p>{format("available_at")}</p>
|
||||
{filterHasAttributes(item.urls, ["url"]).map((url, index) => (
|
||||
<Fragment key={index}>
|
||||
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{format("item_not_available")}</p>
|
||||
|
@ -298,7 +291,7 @@ const LibrarySlug = ({
|
|||
<div className="grid grid-flow-col gap-1">
|
||||
<Chip text={formatLibraryItemType(item.metadata[0])} />
|
||||
{"›"}
|
||||
<Chip text={formatLibraryItemSubType(item.metadata[0])} />
|
||||
<Chip text={prettyItemSubType(item.metadata[0])} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -306,18 +299,24 @@ const LibrarySlug = ({
|
|||
{item.release_date && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{format("release_date")}</h3>
|
||||
<p>{formatDate(item.release_date)}</p>
|
||||
<p>{prettyDate(item.release_date, router.locale)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.price && (
|
||||
<div className="grid place-content-start place-items-center text-center">
|
||||
<h3 className="text-xl">{format("price")}</h3>
|
||||
<p>{formatPrice(item.price)}</p>
|
||||
<p>
|
||||
{prettyPrice(
|
||||
item.price,
|
||||
currencies,
|
||||
item.price.currency?.data?.attributes?.code
|
||||
)}
|
||||
</p>
|
||||
{item.price.currency?.data?.attributes?.code !== currency && (
|
||||
<p>
|
||||
{formatPrice(item.price, currency)}
|
||||
<br />({format("calculated").toLowerCase()})
|
||||
{prettyPrice(item.price, currencies, currency)} <br />(
|
||||
{format("calculated").toLowerCase()})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
@ -331,10 +330,7 @@ const LibrarySlug = ({
|
|||
</h3>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(item.categories.data, ["attributes"]).map((category) => (
|
||||
<Chip
|
||||
key={category.attributes.slug}
|
||||
text={formatCategory(category.attributes.slug, "full")}
|
||||
/>
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -407,60 +403,65 @@ const LibrarySlug = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-4",
|
||||
cIf(!isContentPanelAtLeast3xl, "place-items-center")
|
||||
)}>
|
||||
<h3 className="text-xl">{format("type_information")}</h3>
|
||||
<div className="flex flex-wrap place-content-between gap-x-8">
|
||||
{isDefined(item.metadata[0].page_count) && (
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{format("page", { count: Infinity })}:</p>
|
||||
<p>{item.metadata[0].page_count}</p>
|
||||
</div>
|
||||
)}
|
||||
{item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
|
||||
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-4",
|
||||
cIf(!isContentPanelAtLeast3xl, "place-items-center")
|
||||
)}>
|
||||
<h3 className="text-xl">{format("type_information")}</h3>
|
||||
<div className="flex flex-wrap place-content-between gap-x-8">
|
||||
{item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
|
||||
<>
|
||||
{isDefined(item.metadata[0].page_count) && (
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{format("page", { count: Infinity })}:</p>
|
||||
<p>{item.metadata[0].page_count}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{format("binding")}:</p>
|
||||
<p>
|
||||
{item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Paperback
|
||||
? format("paperback")
|
||||
: item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Hardcover
|
||||
? format("hardcover")
|
||||
: ""}
|
||||
</p>
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{format("binding")}:</p>
|
||||
<p>
|
||||
{item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Paperback
|
||||
? format("paperback")
|
||||
: item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Hardcover
|
||||
? format("hardcover")
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{format("page_order")}:</p>
|
||||
<p>
|
||||
{item.metadata[0].page_order ===
|
||||
Enum_Componentmetadatabooks_Page_Order.LeftToRight
|
||||
? format("left_to_right")
|
||||
: format("right_to_left")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isDefined(item.metadata[0].languages) && (
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">
|
||||
{format("language", {
|
||||
count: item.metadata[0].languages.data.length,
|
||||
})}
|
||||
:
|
||||
</p>
|
||||
{item.metadata[0].languages.data.map((lang) => (
|
||||
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{format("page_order")}:</p>
|
||||
<p>
|
||||
{item.metadata[0].page_order ===
|
||||
Enum_Componentmetadatabooks_Page_Order.LeftToRight
|
||||
? format("left_to_right")
|
||||
: format("right_to_left")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isDefined(item.metadata[0].languages) && (
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">
|
||||
{format("language", {
|
||||
count: item.metadata[0].languages.data.length,
|
||||
})}
|
||||
:
|
||||
</p>
|
||||
{item.metadata[0].languages.data.map((lang) => (
|
||||
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</InsetBox>
|
||||
|
||||
|
@ -493,12 +494,12 @@ const LibrarySlug = ({
|
|||
subitem.attributes.metadata &&
|
||||
subitem.attributes.metadata.length > 0 &&
|
||||
subitem.attributes.metadata[0]
|
||||
? [formatLibraryItemSubType(subitem.attributes.metadata[0])]
|
||||
? [prettyItemSubType(subitem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(subitem.attributes.categories?.data, [
|
||||
"attributes",
|
||||
]).map((category) => formatCategory(category.attributes.slug))}
|
||||
bottomChips={subitem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: subitem.attributes.release_date,
|
||||
price: subitem.attributes.price,
|
||||
|
@ -516,78 +517,58 @@ const LibrarySlug = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{hasContentSection && (
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{format("contents")}</h2>
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
{hasContentScans && (
|
||||
<Button
|
||||
href={`/library/${item.slug}/reader`}
|
||||
icon="auto_stories"
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
)}
|
||||
{item.download_available && (
|
||||
<Button
|
||||
href={getScanArchiveURL(item.slug)}
|
||||
icon="download"
|
||||
text={format("download_archive")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid w-full gap-4">
|
||||
{tracks.map(({ id, title, src }) => (
|
||||
<AudioPlayer key={id} src={src} title={title} />
|
||||
))}
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid items-center",
|
||||
cIf(isContentPanelAtLeast3xl, "grid-cols-[1fr_auto_auto_auto] gap-4", "gap-4")
|
||||
)}>
|
||||
{filterHasAttributes(item.contents?.data, ["attributes"]).map((rangedContent) => (
|
||||
<ContentItem
|
||||
content={
|
||||
rangedContent.attributes.content?.data?.attributes
|
||||
? {
|
||||
translations: filterDefined(
|
||||
rangedContent.attributes.content.data.attributes.translations
|
||||
).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language?.data?.attributes?.code,
|
||||
})),
|
||||
categories: filterHasAttributes(
|
||||
rangedContent.attributes.content.data.attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => formatCategory(category.attributes.slug)),
|
||||
type: rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes
|
||||
? formatContentType(
|
||||
rangedContent.attributes.content.data.attributes.type.data
|
||||
.attributes.slug
|
||||
)
|
||||
: undefined,
|
||||
slug: rangedContent.attributes.content.data.attributes.slug,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rangeStart={
|
||||
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${rangedContent.attributes.range[0].starting_page}`
|
||||
: ""
|
||||
}
|
||||
slug={rangedContent.attributes.slug}
|
||||
parentSlug={item.slug}
|
||||
key={rangedContent.id}
|
||||
hasScanSet={
|
||||
isDefined(rangedContent.attributes.scan_set) &&
|
||||
rangedContent.attributes.scan_set.length > 0
|
||||
}
|
||||
displayType={isContentPanelAtLeast3xl ? "row" : "card"}
|
||||
/>
|
||||
))}
|
||||
{displayOpenScans && (
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Button href={`/library/${item.slug}/reader`} text={format("view_scans")} />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w- grid w-full gap-4">
|
||||
{filterHasAttributes(item.contents.data, ["attributes"]).map((rangedContent) => (
|
||||
<ContentLine
|
||||
content={
|
||||
rangedContent.attributes.content?.data?.attributes
|
||||
? {
|
||||
translations: filterDefined(
|
||||
rangedContent.attributes.content.data.attributes.translations
|
||||
).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language?.data?.attributes?.code,
|
||||
})),
|
||||
categories: filterHasAttributes(
|
||||
rangedContent.attributes.content.data.attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => category.attributes.short),
|
||||
type:
|
||||
rangedContent.attributes.content.data.attributes.type?.data?.attributes
|
||||
?.titles?.[0]?.title ??
|
||||
prettySlug(
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.slug
|
||||
),
|
||||
slug: rangedContent.attributes.content.data.attributes.slug,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rangeStart={
|
||||
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${rangedContent.attributes.range[0].starting_page}`
|
||||
: ""
|
||||
}
|
||||
slug={rangedContent.attributes.slug}
|
||||
parentSlug={item.slug}
|
||||
key={rangedContent.id}
|
||||
hasScanSet={
|
||||
isDefined(rangedContent.attributes.scan_set) &&
|
||||
rangedContent.attributes.scan_set.length > 0
|
||||
}
|
||||
condensed={!isContentPanelAtLeast3xl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -606,11 +587,10 @@ export default LibrarySlug;
|
|||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const { format, formatCategory, formatLibraryItemSubType, formatDate } = getFormat(
|
||||
context.locale
|
||||
);
|
||||
const { format } = getFormat(context.locale);
|
||||
const item = await sdk.getLibraryItem({
|
||||
slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "",
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };
|
||||
sortRangedContent(item.libraryItems.data[0].attributes.contents);
|
||||
|
@ -622,49 +602,23 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
{
|
||||
[format("category", { count: Infinity })]: filterHasAttributes(
|
||||
item.libraryItems.data[0].attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => formatCategory(category.attributes.slug)),
|
||||
["attributes.short"]
|
||||
).map((category) => category.attributes.short),
|
||||
[format("type", { count: Infinity })]: item.libraryItems.data[0].attributes.metadata?.[0]
|
||||
? [formatLibraryItemSubType(item.libraryItems.data[0].attributes.metadata[0])]
|
||||
? [prettyItemSubType(item.libraryItems.data[0].attributes.metadata[0])]
|
||||
: [],
|
||||
[format("release_date")]: [
|
||||
item.libraryItems.data[0].attributes.release_date
|
||||
? formatDate(item.libraryItems.data[0].attributes.release_date)
|
||||
? prettyDate(item.libraryItems.data[0].attributes.release_date, context.locale)
|
||||
: undefined,
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const tracks: Props["tracks"] = ((attributes) => {
|
||||
const metadata = attributes.metadata?.[0];
|
||||
if (metadata?.__typename !== "ComponentMetadataAudio" || !metadata.tracks) {
|
||||
return [];
|
||||
}
|
||||
return filterDefined(metadata.tracks).map((track, index) => ({
|
||||
id: track.slug,
|
||||
src: getTrackURL(attributes.slug, track.slug),
|
||||
title: `${index + 1}. ${track.title}`,
|
||||
}));
|
||||
})(item.libraryItems.data[0].attributes);
|
||||
|
||||
const props: Props = {
|
||||
item: item.libraryItems.data[0].attributes,
|
||||
itemId: item.libraryItems.data[0].id,
|
||||
tracks,
|
||||
openGraph: getOpenGraph(format, title, description, thumbnail?.data?.attributes),
|
||||
isVariantSet:
|
||||
item.libraryItems.data[0].attributes.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.libraryItems.data[0].attributes.metadata[0].subtype?.data?.attributes?.slug ===
|
||||
"variant-set",
|
||||
hasContentScans:
|
||||
item.libraryItems.data[0].attributes.contents?.data.some(
|
||||
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||
) ?? false,
|
||||
hasContentSection:
|
||||
(item.libraryItems.data[0].attributes.contents &&
|
||||
item.libraryItems.data[0].attributes.contents.data.length > 0) ||
|
||||
item.libraryItems.data[0].attributes.download_available ||
|
||||
tracks.length > 0,
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
|
@ -693,7 +647,7 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
|
|||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface ContentItemProps {
|
||||
interface ContentLineProps {
|
||||
content?: {
|
||||
translations: {
|
||||
pre_title: string | null | undefined;
|
||||
|
@ -708,129 +662,126 @@ interface ContentItemProps {
|
|||
rangeStart: string;
|
||||
parentSlug: string;
|
||||
slug: string;
|
||||
|
||||
hasScanSet: boolean;
|
||||
displayType: "card" | "row";
|
||||
condensed: boolean;
|
||||
}
|
||||
|
||||
const ContentItem = ({
|
||||
const ContentLine = ({
|
||||
rangeStart,
|
||||
content,
|
||||
hasScanSet,
|
||||
slug,
|
||||
parentSlug,
|
||||
displayType,
|
||||
}: ContentItemProps): JSX.Element => {
|
||||
condensed,
|
||||
}: ContentLineProps): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const { value: isOpened, toggle: toggleOpened } = useBoolean(false);
|
||||
const [selectedTranslation] = useSmartLanguage({
|
||||
items: content?.translations ?? [],
|
||||
languageExtractor: useCallback(
|
||||
(item: NonNullable<ContentItemProps["content"]>["translations"][number]) => item.language,
|
||||
(item: NonNullable<ContentLineProps["content"]>["translations"][number]) => item.language,
|
||||
[]
|
||||
),
|
||||
});
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (selectedTranslation) {
|
||||
return prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
);
|
||||
}
|
||||
if (isDefined(content)) {
|
||||
return prettySlug(content.slug, parentSlug);
|
||||
}
|
||||
if (slug.endsWith("front-matter")) {
|
||||
return format("front_matter");
|
||||
}
|
||||
if (slug.endsWith("back-matter")) {
|
||||
return format("back_matter");
|
||||
}
|
||||
return prettySlug(slug, parentSlug);
|
||||
}, [content, format, parentSlug, selectedTranslation, slug]);
|
||||
|
||||
if (displayType === "card") {
|
||||
if (condensed) {
|
||||
return (
|
||||
<div className="grid w-full gap-3 rounded-xl bg-highlight p-4 shadow-sm shadow-shade">
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-3">
|
||||
<h3 className="text-lg">{title}</h3>
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-mid" />
|
||||
<p className="text-right">{rangeStart}</p>
|
||||
<div className="my-4 grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
{content?.type && <Chip text={content.type} />}
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30" />
|
||||
<p>{rangeStart}</p>
|
||||
</div>
|
||||
|
||||
{content && (
|
||||
<div>
|
||||
{content.categories && (
|
||||
<div className="flex items-center gap-2">
|
||||
<p>{format("category", { count: content.categories.length })}</p>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content.categories.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content.type && (
|
||||
<div className="flex items-center gap-2">
|
||||
<p>{format("type", { count: 1 })}</p>
|
||||
<Chip className="justify-self-end" text={content.type} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(hasScanSet || content) && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{hasScanSet && (
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
icon="auto_stories"
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button
|
||||
href={`/contents/${content.slug}`}
|
||||
icon="subject"
|
||||
text={format("open_content")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<h3 className="flex flex-wrap place-items-center gap-2">
|
||||
{selectedTranslation
|
||||
? prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)
|
||||
: content
|
||||
? prettySlug(content.slug, parentSlug)
|
||||
: prettySlug(slug, parentSlug)}
|
||||
</h3>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content?.categories?.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{hasScanSet || isDefined(content) ? (
|
||||
<>
|
||||
{hasScanSet && (
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
format("content_is_not_available")
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-[auto_auto_1fr_auto] items-center gap-3">
|
||||
<h3>{title}</h3>
|
||||
<div className="flex flex-wrap place-content-center gap-1">
|
||||
{content?.categories?.map((category, index) => <Chip key={index} text={category} />)}
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-2 rounded-lg px-4",
|
||||
cIf(isOpened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade")
|
||||
)}>
|
||||
<div className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center gap-4">
|
||||
<Link href={""} linkStyled>
|
||||
<h3 className="cursor-pointer" onClick={toggleOpened}>
|
||||
{selectedTranslation
|
||||
? prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)
|
||||
: content
|
||||
? prettySlug(content.slug, parentSlug)
|
||||
: prettySlug(slug, parentSlug)}
|
||||
</h3>
|
||||
</Link>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content?.categories?.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
</div>
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-mid" />
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30" />
|
||||
<p>{rangeStart}</p>
|
||||
{content?.type && <Chip className="justify-self-end" text={content.type} />}
|
||||
</div>
|
||||
<p className="text-right">{rangeStart}</p>
|
||||
<div>
|
||||
{hasScanSet && (
|
||||
<ToolTip content={format("view_scans")}>
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
icon="auto_stories"
|
||||
size="small"
|
||||
/>
|
||||
</ToolTip>
|
||||
<div
|
||||
className={`grid-flow-col place-content-start place-items-center gap-2 ${
|
||||
isOpened ? "grid" : "hidden"
|
||||
}`}>
|
||||
<Ico icon={"subdirectory_arrow_right"} className="text-dark" />
|
||||
|
||||
{hasScanSet || isDefined(content) ? (
|
||||
<>
|
||||
{hasScanSet && (
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
format("content_is_not_available")
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{isDefined(content) && (
|
||||
<ToolTip content={format("open_content")}>
|
||||
<Button href={`/contents/${content.slug}`} icon="subject" size="small" />
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useHotkeys } from "react-hotkeys-hook";
|
|||
import Slider from "rc-slider";
|
||||
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
||||
import { z } from "zod";
|
||||
import { atom } from "jotai";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import {
|
||||
Enum_Componentmetadatabooks_Page_Order as PageOrder,
|
||||
|
@ -38,10 +37,10 @@ import { useFullscreen } from "hooks/useFullscreen";
|
|||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { FilterSettings, useReaderSettings } from "hooks/useReaderSettings";
|
||||
import { useIsWebkit } from "hooks/useIsWebkit";
|
||||
import { useTypedRouter } from "hooks/useTypedRouter";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { getScanArchiveURL } from "helpers/libraryItem";
|
||||
|
||||
type BookType = "book" | "manga";
|
||||
type DisplayMode = "double" | "single";
|
||||
|
@ -71,8 +70,6 @@ const queryParamSchema = z.object({
|
|||
page: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
const isWebKitAtom = atom((get) => get(atoms.userAgent.engine) === "WebKit");
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
|
@ -122,7 +119,7 @@ const LibrarySlug = ({
|
|||
is1ColumnLayout ? "single" : "double"
|
||||
);
|
||||
const router = useTypedRouter(queryParamSchema);
|
||||
const isWebKit = useAtomGetter(isWebKitAtom);
|
||||
const isWebkit = useIsWebkit();
|
||||
|
||||
const { isFullscreen, toggleFullscreen, requestFullscreen } = useFullscreen(Ids.ContentPanel);
|
||||
|
||||
|
@ -282,7 +279,7 @@ const LibrarySlug = ({
|
|||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton title={format("item", { count: 1 })} href={`/library/${itemSlug}`} />
|
||||
<ReturnButton title={format("item", { count: Infinity })} href={`/library/${itemSlug}`} />
|
||||
|
||||
<div className="mt-4 grid gap-2">
|
||||
<WithLabel label={format("paper_texture")}>
|
||||
|
@ -301,7 +298,7 @@ const LibrarySlug = ({
|
|||
<Switch value={isSidePagesEnabled} onClick={toggleIsSidePagesEnabled} />
|
||||
</WithLabel>
|
||||
|
||||
{!isWebKit && (
|
||||
{!isWebkit && (
|
||||
<WithLabel label={format("shadow")}>
|
||||
<Switch value={filterSettings.dropShadow} onClick={toggleDropShadow} />
|
||||
</WithLabel>
|
||||
|
@ -369,14 +366,6 @@ const LibrarySlug = ({
|
|||
sendAnalytics("Reader", "Reset all options");
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.download_available && (
|
||||
<Button
|
||||
href={getScanArchiveURL(item.slug)}
|
||||
icon="download"
|
||||
text={format("download_archive")}
|
||||
/>
|
||||
)}
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
|
@ -396,7 +385,7 @@ const LibrarySlug = ({
|
|||
display: "grid",
|
||||
placeContent: "center",
|
||||
filter:
|
||||
!filterSettings.dropShadow || isWebKit
|
||||
!filterSettings.dropShadow || isWebkit
|
||||
? undefined
|
||||
: isDarkMode
|
||||
? CUSTOM_DARK_DROPSHADOW
|
||||
|
@ -520,13 +509,11 @@ const LibrarySlug = ({
|
|||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon={isGalleryMode ? "expand_more" : "expand_less"}
|
||||
active={isGalleryMode}
|
||||
onClick={() => setIsGalleryMode((current) => !current)}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
icon={isFullscreen ? "fullscreen_exit" : "fullscreen"}
|
||||
active={isFullscreen}
|
||||
onClick={toggleFullscreen}
|
||||
size="small"
|
||||
/>
|
||||
|
@ -593,6 +580,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
const { format } = getFormat(context.locale);
|
||||
const item = await sdk.getLibraryItemScans({
|
||||
slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "",
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!item.libraryItems?.data[0]?.attributes || !item.libraryItems.data[0]?.id)
|
||||
return { notFound: true };
|
||||
|
@ -867,7 +855,6 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
|
|||
{content?.data?.attributes && isDefinedAndNotEmpty(content.data.attributes.slug) && (
|
||||
<Button
|
||||
href={`/contents/${content.data.attributes.slug}`}
|
||||
icon="subject"
|
||||
text={format("open_content")}
|
||||
/>
|
||||
)}
|
||||
|
@ -890,7 +877,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
|
|||
{filterHasAttributes(selectedScan.scanners.data, ["id", "attributes"]).map(
|
||||
(scanner) => (
|
||||
<Fragment key={scanner.id}>
|
||||
<RecorderChip username={scanner.attributes.username} />
|
||||
<RecorderChip recorder={scanner.attributes} />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
|
@ -905,7 +892,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
|
|||
{filterHasAttributes(selectedScan.cleaners.data, ["id", "attributes"]).map(
|
||||
(cleaner) => (
|
||||
<Fragment key={cleaner.id}>
|
||||
<RecorderChip username={cleaner.attributes.username} />
|
||||
<RecorderChip recorder={cleaner.attributes} />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
|
@ -920,7 +907,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
|
|||
{filterHasAttributes(selectedScan.typesetters.data, ["id", "attributes"]).map(
|
||||
(typesetter) => (
|
||||
<Fragment key={typesetter.id}>
|
||||
<RecorderChip username={typesetter.attributes.username} />
|
||||
<RecorderChip recorder={typesetter.attributes} />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue