Compare commits
74 Commits
Author | SHA1 | Date |
---|---|---|
DrMint | e88345f395 | |
DrMint | 34c4570688 | |
DrMint | da916f898a | |
DrMint | 7efa43a630 | |
DrMint | 22e1bf4842 | |
DrMint | d560008cff | |
DrMint | 872f31a6a3 | |
DrMint | 3c7b9aa2d6 | |
DrMint | 62e64b9319 | |
DrMint | e0ee70814d | |
DrMint | 87625ba9ac | |
DrMint | fc1b0c1284 | |
DrMint | 284bbd6272 | |
DrMint | c3796b4fe8 | |
DrMint | 7bde24adaa | |
DrMint | 66dbb29871 | |
DrMint | 6d0429c21a | |
DrMint | 2f0322c1fa | |
DrMint | 6093ef131a | |
DrMint | ff89031123 | |
DrMint | d5e7d704bf | |
DrMint | 22f7c39dff | |
DrMint | a047d18c76 | |
DrMint | 895fee1bae | |
DrMint | 3e979c4005 | |
DrMint | f12d5b0525 | |
DrMint | ef7b3faeca | |
DrMint | d4e6393b9e | |
DrMint | 663bf4f08d | |
DrMint | 06d82e1133 | |
DrMint | f8f98ec41e | |
DrMint | 5d2fe252ec | |
DrMint | a8960d67ed | |
DrMint | ebd3f75804 | |
DrMint | c69b4478f7 | |
DrMint | 5949c8fb8b | |
DrMint | 6a33cfa15a | |
DrMint | c03e92a354 | |
DrMint | b9d10f4670 | |
DrMint | e1e107078e | |
DrMint | 3671264984 | |
DrMint | a52cb1fe54 | |
DrMint | bf6bf2e8a8 | |
DrMint | b9c7c0828a | |
DrMint | 4f78b4f006 | |
DrMint | 9e5ad41e5c | |
DrMint | ca12dc2c29 | |
DrMint | 0c1f252641 | |
DrMint | 6cc6635988 | |
DrMint | 0f6339c0f8 | |
DrMint | 2deea6184e | |
DrMint | cf3837094e | |
DrMint | d19b815275 | |
DrMint | 5be25c656f | |
DrMint | 0f735c62cc | |
DrMint | d68e238b00 | |
DrMint | b6882cd1e5 | |
DrMint | bfb753bf21 | |
DrMint | 113c6566d9 | |
DrMint | e39eb316de | |
DrMint | 7eb7495537 | |
DrMint | 75de7c5f2a | |
DrMint | 5677fb180f | |
DrMint | 5b042a77e2 | |
DrMint | 88a67e4e85 | |
DrMint | 0420dc30a4 | |
DrMint | a0706fd52f | |
DrMint | ffe7e119e0 | |
DrMint | 1fe5952566 | |
DrMint | 7aeb85e4f9 | |
DrMint | df8a7f820d | |
DrMint | fe52ded606 | |
DrMint | 8c98f0796b | |
DrMint | e3e67b8dbc |
|
@ -0,0 +1,44 @@
|
||||||
|
# /!\ 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
|
|
@ -7,7 +7,6 @@ next-env.d.ts
|
||||||
next-sitemap.config.js
|
next-sitemap.config.js
|
||||||
next.config.js
|
next.config.js
|
||||||
postcss.config.js
|
postcss.config.js
|
||||||
tailwind.config.js
|
|
||||||
design.config.js
|
design.config.js
|
||||||
graphql.config.js
|
graphql.config.js
|
||||||
prettier.config.js
|
prettier.config.js
|
|
@ -46,7 +46,7 @@ module.exports = {
|
||||||
"func-style": ["warn", "expression"],
|
"func-style": ["warn", "expression"],
|
||||||
"grouped-accessor-pairs": "warn",
|
"grouped-accessor-pairs": "warn",
|
||||||
"guard-for-in": "warn",
|
"guard-for-in": "warn",
|
||||||
"id-denylist": ["error", "data", "err", "e", "cb", "callback", "i"],
|
"id-denylist": ["error", "err", "e", "cb", "callback", "i"],
|
||||||
// "id-length": "warn",
|
// "id-length": "warn",
|
||||||
"id-match": "warn",
|
"id-match": "warn",
|
||||||
"max-classes-per-file": ["error", 1],
|
"max-classes-per-file": ["error", 1],
|
||||||
|
@ -161,7 +161,6 @@ module.exports = {
|
||||||
"@typescript-eslint/no-invalid-void-type": "error",
|
"@typescript-eslint/no-invalid-void-type": "error",
|
||||||
"@typescript-eslint/no-meaningless-void-operator": "error",
|
"@typescript-eslint/no-meaningless-void-operator": "error",
|
||||||
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "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-require-imports": "error",
|
||||||
// "@typescript-eslint/no-type-alias": "warn",
|
// "@typescript-eslint/no-type-alias": "warn",
|
||||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
|
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
|
||||||
|
@ -182,7 +181,6 @@ module.exports = {
|
||||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||||
"@typescript-eslint/promise-function-async": "error",
|
"@typescript-eslint/promise-function-async": "error",
|
||||||
"@typescript-eslint/require-array-sort-compare": "error",
|
"@typescript-eslint/require-array-sort-compare": "error",
|
||||||
"@typescript-eslint/sort-type-union-intersection-members": "warn",
|
|
||||||
// "@typescript-eslint/strict-boolean-expressions": [
|
// "@typescript-eslint/strict-boolean-expressions": [
|
||||||
// "error",
|
// "error",
|
||||||
// { allowAny: true },
|
// { allowAny: true },
|
||||||
|
@ -192,7 +190,6 @@ module.exports = {
|
||||||
"@typescript-eslint/unified-signatures": "error",
|
"@typescript-eslint/unified-signatures": "error",
|
||||||
|
|
||||||
/* EXTENSION OF ESLINT */
|
/* EXTENSION OF ESLINT */
|
||||||
"@typescript-eslint/no-duplicate-imports": "error",
|
|
||||||
"@typescript-eslint/default-param-last": "warn",
|
"@typescript-eslint/default-param-last": "warn",
|
||||||
"@typescript-eslint/dot-notation": "warn",
|
"@typescript-eslint/dot-notation": "warn",
|
||||||
"@typescript-eslint/init-declarations": "warn",
|
"@typescript-eslint/init-declarations": "warn",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Generated content
|
# Generated content
|
||||||
src/graphql/generated.ts
|
src/graphql/generated.ts
|
||||||
|
|
||||||
|
public/robots.txt
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"upgrade": false,
|
|
||||||
"reject": ["react-hotkeys-hook"]
|
|
||||||
}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
upgrade: true
|
||||||
|
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.
|
|
@ -1 +1,2 @@
|
||||||
.next
|
.next
|
||||||
|
public/local-data/*
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"css.lint.unknownAtRules": "ignore",
|
"css.lint.unknownAtRules": "ignore",
|
||||||
"editor.rulers": [100]
|
"editor.rulers": [100],
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
}
|
}
|
||||||
|
|
43
README.md
43
README.md
|
@ -28,9 +28,10 @@ Our Content Management System (CMS) that uses [Strapi](https://strapi.io/).
|
||||||
|
|
||||||
### [img.accords-library.com](https://github.com/Accords-Library/img.accords-library.com)
|
### [img.accords-library.com](https://github.com/Accords-Library/img.accords-library.com)
|
||||||
|
|
||||||
A custom made image processor to overcome the lack of customization offered by Strapi build-in image processor. There is a python script to bulk process all images uploaded to Strapi. Subsequent changes to Strapi's media library can be handled using webhooks. The repo includes a server that listen to these webhook calls, and another to serve the images.
|
A custom made image processor to overcome the lack of customization offered by Strapi build-in image processor. There is a python script to bulk process all images uploaded to Strapi. Subsequent changes to Strapi's media library can be handled using webhooks. The repo includes a server that listen to these webhook calls, and another to serve the images.
|
||||||
|
|
||||||
Each image in Strapi's media library is converted to four different formats:
|
Each image in Strapi's media library is converted to four different formats:
|
||||||
|
|
||||||
- Small: 512x512, quality 60, .webp
|
- Small: 512x512, quality 60, .webp
|
||||||
- Medium: 1024x1024, quality 75, .webp
|
- Medium: 1024x1024, quality 75, .webp
|
||||||
- Large: 2048x2048, quality 80, .webp
|
- Large: 2048x2048, quality 80, .webp
|
||||||
|
@ -39,7 +40,7 @@ Each image in Strapi's media library is converted to four different formats:
|
||||||
### [search.accords-library.com](https://github.com/Accords-Library/search.accords-library.com)
|
### [search.accords-library.com](https://github.com/Accords-Library/search.accords-library.com)
|
||||||
|
|
||||||
A search engine that uses [Meilisearch](https://www.meilisearch.com/).
|
A search engine that uses [Meilisearch](https://www.meilisearch.com/).
|
||||||
The repo includes a docker-compose file to run an instance of Meilisearch. There is also a server that populates Meilisearch's documents at startup, then listen to webhooks sent by Strapi for subsequent changes.
|
The repo includes a docker-compose file to run an instance of Meilisearch. There is also a server that populates Meilisearch's documents at startup, then listen to webhooks sent by Strapi for subsequent changes.
|
||||||
|
|
||||||
### [gallery.accords-library.com](https://github.com/Accords-Library/gallery.accords-library.com)
|
### [gallery.accords-library.com](https://github.com/Accords-Library/gallery.accords-library.com)
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ A detailled look at the technologies used in this repository:
|
||||||
- The website is built before running in production
|
- The website is built before running in production
|
||||||
- Performances are great, and it's possible to deploy the app on a CDN
|
- 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
|
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
|
||||||
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every pages.
|
- 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.
|
||||||
|
|
||||||
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
|
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
|
||||||
|
|
||||||
|
@ -101,17 +102,15 @@ A detailled look at the technologies used in this repository:
|
||||||
|
|
||||||
- Multilingual
|
- Multilingual
|
||||||
|
|
||||||
- By default, use the browser's language as the main language
|
- 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.
|
||||||
- Fallback languages are used for content which are not available in the main language
|
- 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.
|
||||||
- Main and fallback languages can be ordered manually by the user
|
- 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.
|
||||||
- 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
|
- UI Localizations
|
||||||
|
|
||||||
- The translated wordings use [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to include variables, plural, dates...
|
- 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
|
- Use a custom ICU Typescript transformation script to provide type safety when formatting ICU wordings
|
||||||
- Fallback to English if a specific working isn't available in the user's language
|
- Fallback to English if the translation is missing.
|
||||||
|
|
||||||
- SEO
|
- SEO
|
||||||
|
|
||||||
|
@ -133,13 +132,13 @@ 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...
|
- [ts-unused-exports](https://www.npmjs.com/package/ts-unused-exports) to find unused exported functions/constants...
|
||||||
|
|
||||||
- Other
|
- Other
|
||||||
|
|
||||||
- Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader)
|
- Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader)
|
||||||
- Support for [Material Symbols](https://fonts.google.com/icons)
|
- 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)
|
- Custom lightbox using [react-zoom-pan-pinch](https://www.npmjs.com/package/react-zoom-pan-pinch)
|
||||||
- Handle query params using [Zod](https://zod.dev/)
|
- Handle query params type-validation using [Zod](https://zod.dev/)
|
||||||
- A secret "Terminal" mode. Can you find it?
|
- A secret "Terminal" mode. Can you find it?
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -148,30 +147,14 @@ cd accords-library.com
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a env file:
|
Create a env file based on the example one:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cp .env.example .env.local
|
||||||
nano .env.local
|
nano .env.local
|
||||||
```
|
```
|
||||||
|
|
||||||
Enter the following information:
|
Change the variables
|
||||||
|
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Run in dev mode:
|
Run in dev mode:
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ const colors = {
|
||||||
light: { r: 255, g: 237, b: 216 },
|
light: { r: 255, g: 237, b: 216 },
|
||||||
mid: { r: 240, g: 209, b: 179 },
|
mid: { r: 240, g: 209, b: 179 },
|
||||||
dark: { r: 156, g: 102, b: 68 },
|
dark: { r: 156, g: 102, b: 68 },
|
||||||
shade: { r: 156, g: 102, b: 68 },
|
shade: { r: 192, g: 132, b: 94 },
|
||||||
black: { r: 27, g: 24, b: 17 },
|
black: { r: 27, g: 24, b: 17 },
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
|
@ -12,7 +12,7 @@ const colors = {
|
||||||
light: { r: 38, g: 34, b: 30 },
|
light: { r: 38, g: 34, b: 30 },
|
||||||
mid: { r: 57, g: 45, b: 34 },
|
mid: { r: 57, g: 45, b: 34 },
|
||||||
dark: { r: 192, g: 132, b: 94 },
|
dark: { r: 192, g: 132, b: 94 },
|
||||||
shade: { r: 0, g: 0, b: 0 },
|
shade: { r: 25, g: 25, b: 20 },
|
||||||
black: { r: 235, g: 234, b: 231 },
|
black: { r: 235, g: 234, b: 231 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* CONFIG */
|
/* CONFIG */
|
||||||
|
|
||||||
const locales = ["en", "es", "fr", "pt-br", "ja"];
|
const locales = ["en", "es", "fr", "pt-br", "ja", "zh"];
|
||||||
|
|
||||||
/* END CONFIG */
|
/* END CONFIG */
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
115
package.json
115
package.json
|
@ -2,11 +2,13 @@
|
||||||
"name": "accords-library.com",
|
"name": "accords-library.com",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "patch-package",
|
||||||
"dev": "next dev -p 12499",
|
"dev": "next dev -p 12499",
|
||||||
"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!",
|
"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-exports": "ts-unused-exports ./tsconfig.json --excludePathsFromReport=src/pages --ignoreFiles=generated",
|
"unused-wording-keys": "esrun --send-code-mode=temporaryFile src/graphql/unusedWordingKeys.ts --uwk",
|
||||||
"fetch-local-data": "npm run generate && esrun src/graphql/fetchLocalData.ts --esrun",
|
"unused-exports": "ts-unused-exports ./tsconfig.json --excludePathsFromReport='src/pages;tailwind.config.ts;src/graphql/generated.ts;src/shared/meilisearch-graphql-typings/generated.ts'",
|
||||||
"icu-to-ts": "esrun src/graphql/icuToTypescript.ts --icu",
|
"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",
|
||||||
"prebuild": "npm run fetch-local-data && npm run icu-to-ts",
|
"prebuild": "npm run fetch-local-data && npm run icu-to-ts",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"postbuild": "next-sitemap --config next-sitemap.config.js",
|
"postbuild": "next-sitemap --config next-sitemap.config.js",
|
||||||
|
@ -15,74 +17,75 @@
|
||||||
"eslint": "npx eslint .",
|
"eslint": "npx eslint .",
|
||||||
"generate": "graphql-codegen --config graphql-codegen.config.js",
|
"generate": "graphql-codegen --config graphql-codegen.config.js",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"prettier": "prettier --end-of-line auto --write ."
|
"prettier": "prettier --list-different --end-of-line auto --write .",
|
||||||
|
"upgrade": "ncu"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/opendyslexic": "^4.5.4",
|
"@fontsource/noto-serif-jp": "^5.0.7",
|
||||||
"@fontsource/share-tech-mono": "^4.5.9",
|
"@fontsource/opendyslexic": "^5.0.7",
|
||||||
"@fontsource/vollkorn": "^4.5.14",
|
"@fontsource/share-tech-mono": "^5.0.8",
|
||||||
"@fontsource/zen-maru-gothic": "^4.5.16",
|
"@fontsource/vollkorn": "^5.0.9",
|
||||||
"@formatjs/icu-messageformat-parser": "^2.1.14",
|
"@fontsource/zen-maru-gothic": "^5.0.7",
|
||||||
|
"@formatjs/icu-messageformat-parser": "^2.6.0",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.15",
|
||||||
"cuid": "^2.1.8",
|
"cuid": "^2.1.8",
|
||||||
"intl-messageformat": "^10.2.5",
|
"html-to-text": "^9.0.5",
|
||||||
"isomorphic-dompurify": "^0.26.0",
|
"intl-messageformat": "^10.5.0",
|
||||||
"jotai": "^1.13.1",
|
"isomorphic-dompurify": "^1.8.0",
|
||||||
"markdown-to-jsx": "^7.1.8",
|
"jotai": "^2.3.1",
|
||||||
"marked": "^4.2.12",
|
"markdown-to-jsx": "^7.3.2",
|
||||||
"material-symbols": "^0.4.2",
|
"marked": "^7.0.3",
|
||||||
"meilisearch": "^0.30.0",
|
"material-symbols": "^0.10.4",
|
||||||
"next": "^13.1.5",
|
"meilisearch": "^0.34.1",
|
||||||
"nodemailer": "^6.9.0",
|
"next": "^13.4.17",
|
||||||
"rc-slider": "^10.1.0",
|
"nodemailer": "^6.9.4",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
|
"rc-slider": "^10.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-collapsible": "^2.10.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "^3.4.7",
|
||||||
"react-swipeable": "^7.0.0",
|
"react-swipeable": "^7.0.1",
|
||||||
"react-zoom-pan-pinch": "^2.2.1",
|
"react-zoom-pan-pinch": "^3.1.0",
|
||||||
"string-natural-compare": "^3.0.1",
|
"string-natural-compare": "^3.0.1",
|
||||||
"throttle-debounce": "^5.0.0",
|
"throttle-debounce": "^5.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"turndown": "^7.1.1",
|
"turndown": "^7.1.2",
|
||||||
"ua-parser-js": "^1.0.33",
|
"ua-parser-js": "^1.0.35",
|
||||||
"usehooks-ts": "^2.9.1",
|
"usehooks-ts": "^2.9.1",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.22.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@digitak/esrun": "^3.2.18",
|
"@digitak/esrun": "3.2.24",
|
||||||
"@graphql-codegen/cli": "^2.16.4",
|
"@graphql-codegen/cli": "5.0.0",
|
||||||
"@graphql-codegen/typescript": "2.8.7",
|
"@graphql-codegen/typescript": "4.0.1",
|
||||||
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
|
"@graphql-codegen/typescript-graphql-request": "5.0.0",
|
||||||
"@graphql-codegen/typescript-operations": "^2.5.12",
|
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||||
"@types/marked": "^4.0.8",
|
"@types/html-to-text": "^9.0.1",
|
||||||
"@types/node": "18.11.18",
|
"@types/marked": "^5.0.1",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/node": "20.5.0",
|
||||||
"@types/react": "^18.0.27",
|
"@types/nodemailer": "^6.4.9",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react": "^18.2.20",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/string-natural-compare": "^3.0.2",
|
"@types/string-natural-compare": "^3.0.2",
|
||||||
"@types/throttle-debounce": "^5.0.0",
|
"@types/throttle-debounce": "^5.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||||
"@typescript-eslint/parser": "^5.49.0",
|
"@typescript-eslint/parser": "^6.4.0",
|
||||||
"dotenv": "^16.0.3",
|
"chalk": "^5.3.0",
|
||||||
"eslint": "^8.32.0",
|
"dotenv": "^16.3.1",
|
||||||
"eslint-config-next": "13.1.5",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-config-next": "13.4.17",
|
||||||
"graphql": "^16.6.0",
|
"eslint-plugin-import": "^2.28.0",
|
||||||
"graphql-request": "^5.1.0",
|
"graphql": "16.8.0",
|
||||||
"next-sitemap": "^3.1.47",
|
"graphql-request": "6.1.0",
|
||||||
"prettier": "^2.8.3",
|
"next-sitemap": "^4.2.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier": "^3.0.2",
|
||||||
"tailwindcss": "^3.2.4",
|
"prettier-plugin-tailwindcss": "^0.5.3",
|
||||||
"ts-unused-exports": "^9.0.2",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^4.9.4"
|
"ts-unused-exports": "^10.0.0",
|
||||||
},
|
"typescript": "^5.1.6"
|
||||||
"overrides": {
|
|
||||||
"react-zoom-pan-pinch": {
|
|
||||||
"react": "$react",
|
|
||||||
"react-dom": "$react-dom"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -1,91 +1 @@
|
||||||
{
|
{"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,36 +1 @@
|
||||||
{
|
{"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,6 +1,8 @@
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useSwipeable } from "react-swipeable";
|
import { useSwipeable } from "react-swipeable";
|
||||||
import { MaterialSymbol } from "material-symbols";
|
import { MaterialSymbol } from "material-symbols";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { layout } from "../../design.config";
|
import { layout } from "../../design.config";
|
||||||
import { Ico } from "./Ico";
|
import { Ico } from "./Ico";
|
||||||
import { MainPanel } from "./Panels/MainPanel";
|
import { MainPanel } from "./Panels/MainPanel";
|
||||||
|
@ -18,6 +20,7 @@ import { useFormat } from "hooks/useFormat";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SENSIBILITY_SWIPE = 1.1;
|
const SENSIBILITY_SWIPE = 1.1;
|
||||||
|
const isIOSAtom = atom((get) => get(atoms.userAgent.os) === "iOS");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -48,11 +51,13 @@ export const AppLayout = ({
|
||||||
const [isSubPanelOpened, setSubPanelOpened] = useAtomPair(atoms.layout.subPanelOpened);
|
const [isSubPanelOpened, setSubPanelOpened] = useAtomPair(atoms.layout.subPanelOpened);
|
||||||
const [isMainPanelOpened, setMainPanelOpened] = useAtomPair(atoms.layout.mainPanelOpened);
|
const [isMainPanelOpened, setMainPanelOpened] = useAtomPair(atoms.layout.mainPanelOpened);
|
||||||
const isMenuGesturesEnabled = useAtomGetter(atoms.layout.menuGesturesEnabled);
|
const isMenuGesturesEnabled = useAtomGetter(atoms.layout.menuGesturesEnabled);
|
||||||
|
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||||
const { format } = useFormat();
|
|
||||||
|
|
||||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||||
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
|
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
|
||||||
|
const isIOS = useAtomGetter(isIOSAtom);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { format } = useFormat();
|
||||||
|
|
||||||
const handlers = useSwipeable({
|
const handlers = useSwipeable({
|
||||||
onSwipedLeft: (SwipeEventData) => {
|
onSwipedLeft: (SwipeEventData) => {
|
||||||
|
@ -77,15 +82,19 @@ export const AppLayout = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel);
|
const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel) && is1ColumnLayout;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...handlers}
|
{...handlers}
|
||||||
id={Ids.Body}
|
id={Ids.Body}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"fixed inset-0 m-0 grid touch-pan-y bg-light p-0 [grid-template-areas:'main_sub_content']",
|
"fixed inset-0 m-0 grid touch-pan-y bg-light p-0",
|
||||||
cIf(is1ColumnLayout, "grid-rows-[1fr_5rem] [grid-template-areas:'content''navbar']")
|
cIf(
|
||||||
|
is1ColumnLayout,
|
||||||
|
"grid-rows-[1fr_5rem] [grid-template-areas:'content''navbar']",
|
||||||
|
"[grid-template-areas:'main_sub_content']"
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: is1ColumnLayout
|
gridTemplateColumns: is1ColumnLayout
|
||||||
|
@ -98,29 +107,69 @@ export const AppLayout = ({
|
||||||
<title>{openGraph.title}</title>
|
<title>{openGraph.title}</title>
|
||||||
<meta name="description" content={openGraph.description} />
|
<meta name="description" content={openGraph.description} />
|
||||||
|
|
||||||
|
<meta name="twitter:site" content="@AccordsLibrary" />
|
||||||
<meta name="twitter:title" content={openGraph.title} />
|
<meta name="twitter:title" content={openGraph.title} />
|
||||||
<meta name="twitter:description" content={openGraph.description} />
|
<meta name="twitter:description" content={openGraph.description} />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:image" content={openGraph.thumbnail.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:title" content={openGraph.title} />
|
||||||
<meta property="og:description" content={openGraph.description} />
|
<meta property="og:description" content={openGraph.description} />
|
||||||
|
|
||||||
<meta property="og:image" content={openGraph.thumbnail.image} />
|
<meta property="og:image" content={openGraph.thumbnail.image} />
|
||||||
<meta property="og:image:secure_url" 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:width" content={openGraph.thumbnail.width.toString()} />
|
||||||
<meta property="og:image:height" content={openGraph.thumbnail.height.toString()} />
|
<meta property="og:image:height" content={openGraph.thumbnail.height.toString()} />
|
||||||
<meta property="og:image:alt" content={openGraph.thumbnail.alt} />
|
<meta property="og:image:alt" content={openGraph.thumbnail.alt} />
|
||||||
<meta property="og:image:type" content="image/jpeg" />
|
<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>
|
</Head>
|
||||||
|
|
||||||
|
{/* Content panel */}
|
||||||
|
<div
|
||||||
|
id={Ids.ContentPanel}
|
||||||
|
className={cJoin(
|
||||||
|
"bg-light [grid-area:content]",
|
||||||
|
cIf(!isIOS, "texture-paper-dots"),
|
||||||
|
cIf(contentPanelScroolbar, "overflow-y-scroll")
|
||||||
|
)}>
|
||||||
|
{isDefined(contentPanel) ? (
|
||||||
|
contentPanel
|
||||||
|
) : turnSubIntoContent ? (
|
||||||
|
subPanel
|
||||||
|
) : (
|
||||||
|
<ContentPlaceholder message={format("select_option_sidebar")} icon={"chevron_left"} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Background when navbar is opened */}
|
{/* Background when navbar is opened */}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`absolute inset-0 transition-filter duration-500
|
`absolute inset-0 z-40 transition-filter duration-500
|
||||||
[grid-area:content]`,
|
[grid-area:content]`,
|
||||||
cIf(
|
cIf(
|
||||||
(isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout,
|
(isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout,
|
||||||
"z-10 backdrop-blur",
|
cIf(!isPerfModeEnabled, "backdrop-blur"),
|
||||||
"pointer-events-none touch-none"
|
"pointer-events-none touch-none"
|
||||||
)
|
)
|
||||||
)}>
|
)}>
|
||||||
|
@ -140,57 +189,12 @@ export const AppLayout = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content panel */}
|
|
||||||
<div
|
|
||||||
id={Ids.ContentPanel}
|
|
||||||
className={cJoin(
|
|
||||||
"bg-light texture-paper-dots [grid-area:content]",
|
|
||||||
cIf(contentPanelScroolbar, "overflow-y-scroll")
|
|
||||||
)}>
|
|
||||||
{isDefined(contentPanel) ? (
|
|
||||||
contentPanel
|
|
||||||
) : (
|
|
||||||
<ContentPlaceholder message={format("select_option_sidebar")} icon={"chevron_left"} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sub panel */}
|
|
||||||
{isDefined(subPanel) && (
|
|
||||||
<div
|
|
||||||
id={Ids.SubPanel}
|
|
||||||
className={cJoin(
|
|
||||||
`z-20 overflow-y-scroll border-r border-dark/50 bg-light
|
|
||||||
transition-transform duration-300 scrollbar-none texture-paper-dots`,
|
|
||||||
cIf(
|
|
||||||
is1ColumnLayout,
|
|
||||||
"justify-self-end border-r-0 [grid-area:content]",
|
|
||||||
"[grid-area:sub]"
|
|
||||||
),
|
|
||||||
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l"),
|
|
||||||
cIf(is1ColumnLayout && !isSubPanelOpened && !turnSubIntoContent, "translate-x-[100vw]"),
|
|
||||||
cIf(is1ColumnLayout && turnSubIntoContent, "w-full border-l-0")
|
|
||||||
)}>
|
|
||||||
{subPanel}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main panel */}
|
|
||||||
<div
|
|
||||||
className={cJoin(
|
|
||||||
`z-30 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")
|
|
||||||
)}>
|
|
||||||
<MainPanel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`z-10 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
|
`z-40 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
|
||||||
border-dotted border-black bg-light texture-paper-dots [grid-area:navbar]`,
|
border-dotted border-black bg-light [grid-area:navbar]`,
|
||||||
|
cIf(!isIOS, "texture-paper-dots"),
|
||||||
cIf(!is1ColumnLayout, "hidden")
|
cIf(!is1ColumnLayout, "hidden")
|
||||||
)}>
|
)}>
|
||||||
<Ico
|
<Ico
|
||||||
|
@ -221,11 +225,47 @@ export const AppLayout = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sub panel */}
|
||||||
|
{isDefined(subPanel) && !turnSubIntoContent && (
|
||||||
|
<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"),
|
||||||
|
cIf(
|
||||||
|
is1ColumnLayout,
|
||||||
|
"z-40 justify-self-end border-r-0 [grid-area:content]",
|
||||||
|
"[grid-area:sub]"
|
||||||
|
),
|
||||||
|
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l"),
|
||||||
|
cIf(is1ColumnLayout && !isSubPanelOpened, "translate-x-[100vw]")
|
||||||
|
)}>
|
||||||
|
{subPanel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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]"),
|
||||||
|
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)]"),
|
||||||
|
cIf(!isMainPanelOpened && is1ColumnLayout, "-translate-x-full")
|
||||||
|
)}>
|
||||||
|
<MainPanel />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
/*
|
||||||
|
* ╭──────────────────────╮
|
||||||
|
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface ContentPlaceholderProps {
|
interface ContentPlaceholderProps {
|
||||||
message: string;
|
message: string;
|
||||||
|
|
|
@ -16,8 +16,7 @@ export const Chip = ({ className, text }: Props): JSX.Element => (
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`grid place-content-center place-items-center whitespace-nowrap rounded-full border
|
`grid place-content-center place-items-center whitespace-nowrap rounded-full border
|
||||||
px-1.5 pb-[0.14rem] text-xs opacity-70 transition-[color,opacity,border-color]
|
border-black/70 px-1.5 pb-[0.14rem] text-xs text-black/70`,
|
||||||
hover:opacity-100`,
|
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback } from "react";
|
import { MouseEventHandler, useCallback } from "react";
|
||||||
import { DatePickerFragment } from "graphql/generated";
|
import { DatePickerFragment } from "graphql/generated";
|
||||||
import { TranslatedProps } from "types/TranslatedProps";
|
import { TranslatedProps } from "types/TranslatedProps";
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
|
@ -17,12 +17,23 @@ interface Props {
|
||||||
url: string;
|
url: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChroniclePreview = ({ date, url, title, active, disabled }: Props): JSX.Element => (
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
export const ChroniclePreview = ({
|
||||||
|
date,
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
active,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
}: Props): JSX.Element => (
|
||||||
<DownPressable
|
<DownPressable
|
||||||
className="flex w-full gap-4 py-4 px-5"
|
className="flex w-full gap-4 px-5 py-4"
|
||||||
href={url}
|
href={url}
|
||||||
|
onClick={onClick}
|
||||||
active={active}
|
active={active}
|
||||||
border
|
border
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useBoolean } from "usehooks-ts";
|
import Collapsible from "react-collapsible";
|
||||||
import { TranslatedChroniclePreview } from "./ChroniclePreview";
|
import { TranslatedChroniclePreview } from "./ChroniclePreview";
|
||||||
import { GetChroniclesChaptersQuery } from "graphql/generated";
|
import { GetChroniclesChaptersQuery } from "graphql/generated";
|
||||||
import { filterHasAttributes } from "helpers/asserts";
|
import { filterHasAttributes } from "helpers/asserts";
|
||||||
import { prettyInlineTitle, prettySlug, sJoin } from "helpers/formatters";
|
import { prettyInlineTitle, prettySlug, sJoin } from "helpers/formatters";
|
||||||
import { Ico } from "components/Ico";
|
|
||||||
import { compareDate } from "helpers/date";
|
import { compareDate } from "helpers/date";
|
||||||
import { TranslatedProps } from "types/TranslatedProps";
|
import { TranslatedProps } from "types/TranslatedProps";
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
|
import { useAtomSetter } from "helpers/atoms";
|
||||||
|
import { atoms } from "contexts/atoms";
|
||||||
|
import { Button } from "components/Inputs/Button";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -22,28 +24,42 @@ interface Props {
|
||||||
>["data"];
|
>["data"];
|
||||||
currentSlug?: string;
|
currentSlug?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
open?: boolean;
|
||||||
|
onTriggerClosing?: () => void;
|
||||||
|
onOpening?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element => {
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
const { value: isOpen, toggle: toggleOpen } = useBoolean(
|
|
||||||
chronicles.some((chronicle) => chronicle.attributes?.slug === currentSlug)
|
const ChroniclesList = ({
|
||||||
);
|
chronicles,
|
||||||
|
currentSlug,
|
||||||
|
title,
|
||||||
|
open,
|
||||||
|
onTriggerClosing,
|
||||||
|
onOpening,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="grid place-content-center">
|
<Collapsible
|
||||||
<div className="grid cursor-pointer grid-cols-[1em_1fr] gap-4" onClick={toggleOpen}>
|
open={open}
|
||||||
<Ico className="!text-xl" icon={isOpen ? "arrow_drop_up" : "arrow_drop_down"} />
|
accordionPosition={title}
|
||||||
<p className="mb-4 font-headers text-xl">{title}</p>
|
contentInnerClassName="grid gap-4 pt-4"
|
||||||
</div>
|
onTriggerClosing={onTriggerClosing}
|
||||||
</div>
|
onOpening={onOpening}
|
||||||
<div
|
easing="ease-in-out"
|
||||||
className="grid gap-4 overflow-hidden transition-height duration-500"
|
transitionTime={400}
|
||||||
style={{ maxHeight: isOpen ? `${8 * chronicles.length}rem` : 0 }}>
|
lazyRender
|
||||||
{filterHasAttributes(chronicles, [
|
contentHiddenWhenClosed
|
||||||
"attributes.contents",
|
trigger={
|
||||||
"attributes.translations",
|
<div className="flex place-content-center place-items-center gap-4">
|
||||||
] as const)
|
<h2 className="text-center text-xl">{title}</h2>
|
||||||
|
<Button icon={open ? "expand_less" : "expand_more"} active={open} size="small" />
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
{filterHasAttributes(chronicles, ["attributes.contents", "attributes.translations"])
|
||||||
.sort((a, b) => compareDate(a.attributes.date_start, b.attributes.date_start))
|
.sort((a, b) => compareDate(a.attributes.date_start, b.attributes.date_start))
|
||||||
.map((chronicle) => (
|
.map((chronicle) => (
|
||||||
<div key={chronicle.id} id={`chronicle-${chronicle.attributes.slug}`}>
|
<div key={chronicle.id} id={`chronicle-${chronicle.attributes.slug}`}>
|
||||||
|
@ -51,14 +67,14 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
||||||
chronicle.attributes.contents.data.length === 1
|
chronicle.attributes.contents.data.length === 1
|
||||||
? filterHasAttributes(chronicle.attributes.contents.data, [
|
? filterHasAttributes(chronicle.attributes.contents.data, [
|
||||||
"attributes.translations",
|
"attributes.translations",
|
||||||
] as const).map((content, index) => (
|
]).map((content, index) => (
|
||||||
<TranslatedChroniclePreview
|
<TranslatedChroniclePreview
|
||||||
key={index}
|
key={index}
|
||||||
active={chronicle.attributes.slug === currentSlug}
|
active={chronicle.attributes.slug === currentSlug}
|
||||||
date={chronicle.attributes.date_start}
|
date={chronicle.attributes.date_start}
|
||||||
translations={filterHasAttributes(content.attributes.translations, [
|
translations={filterHasAttributes(content.attributes.translations, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
] as const).map((translation) => ({
|
]).map((translation) => ({
|
||||||
title: prettyInlineTitle(
|
title: prettyInlineTitle(
|
||||||
translation.pre_title,
|
translation.pre_title,
|
||||||
translation.title,
|
translation.title,
|
||||||
|
@ -75,6 +91,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
||||||
"/#chronicle-",
|
"/#chronicle-",
|
||||||
chronicle.attributes.slug
|
chronicle.attributes.slug
|
||||||
)}
|
)}
|
||||||
|
onClick={() => setSubPanelOpened(false)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: chronicle.attributes.translations.length > 0 && (
|
: chronicle.attributes.translations.length > 0 && (
|
||||||
|
@ -84,7 +101,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
||||||
translations={filterHasAttributes(chronicle.attributes.translations, [
|
translations={filterHasAttributes(chronicle.attributes.translations, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
"title",
|
"title",
|
||||||
] as const).map((translation) => ({
|
]).map((translation) => ({
|
||||||
title: translation.title,
|
title: translation.title,
|
||||||
language: translation.language.data.attributes.code,
|
language: translation.language.data.attributes.code,
|
||||||
}))}
|
}))}
|
||||||
|
@ -101,7 +118,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { GetChroniclesChaptersQuery } from "graphql/generated";
|
||||||
|
import { filterHasAttributes } from "helpers/asserts";
|
||||||
|
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
|
||||||
|
import { prettySlug } from "helpers/formatters";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
chapters: NonNullable<GetChroniclesChaptersQuery["chroniclesChapters"]>["data"];
|
||||||
|
currentChronicleSlug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: -1
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-16">
|
||||||
|
{filterHasAttributes(chapters, ["attributes.chronicles", "id"]).map(
|
||||||
|
(chapter, chapterIndex) => (
|
||||||
|
<TranslatedChroniclesList
|
||||||
|
currentSlug={currentChronicleSlug}
|
||||||
|
open={openedIndex === chapterIndex}
|
||||||
|
onOpening={() => setOpenedIndex(chapterIndex)}
|
||||||
|
onTriggerClosing={() => setOpenedIndex(-1)}
|
||||||
|
key={chapter.id}
|
||||||
|
chronicles={chapter.attributes.chronicles.data}
|
||||||
|
translations={filterHasAttributes(chapter.attributes.titles, [
|
||||||
|
"language.data.attributes.code",
|
||||||
|
]).map((translation) => ({
|
||||||
|
title: translation.title,
|
||||||
|
language: translation.language.data.attributes.code,
|
||||||
|
}))}
|
||||||
|
fallback={{ title: prettySlug(chapter.attributes.slug) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,9 +1,10 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { atom } from "jotai";
|
||||||
import { cJoin, cIf } from "helpers/className";
|
import { cJoin, cIf } from "helpers/className";
|
||||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomSetter, useAtomPair } from "helpers/atoms";
|
import { useAtomSetter, useAtomPair, atomPairing } from "helpers/atoms";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -12,6 +13,9 @@ import { useAtomSetter, useAtomPair } from "helpers/atoms";
|
||||||
|
|
||||||
const LINE_PREFIX = "root@accords-library.com:";
|
const LINE_PREFIX = "root@accords-library.com:";
|
||||||
|
|
||||||
|
const previousLinesAtom = atomPairing(atom<string[]>([]));
|
||||||
|
const previousCommandsAtom = atomPairing(atom<string[]>([]));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
@ -33,8 +37,8 @@ export const Terminal = ({
|
||||||
const [childrenPaths, setChildrenPaths] = useState(propsChildrenPaths);
|
const [childrenPaths, setChildrenPaths] = useState(propsChildrenPaths);
|
||||||
const setPlayerName = useAtomSetter(atoms.settings.playerName);
|
const setPlayerName = useAtomSetter(atoms.settings.playerName);
|
||||||
|
|
||||||
const [previousCommands, setPreviousCommands] = useAtomPair(atoms.terminal.previousCommands);
|
const [previousCommands, setPreviousCommands] = useAtomPair(previousCommandsAtom);
|
||||||
const [previousLines, setPreviousLines] = useAtomPair(atoms.terminal.previousLines);
|
const [previousLines, setPreviousLines] = useAtomPair(previousLinesAtom);
|
||||||
|
|
||||||
const [line, setLine] = useState("");
|
const [line, setLine] = useState("");
|
||||||
const [displayCurrentLine, setDisplayCurrentLine] = useState(true);
|
const [displayCurrentLine, setDisplayCurrentLine] = useState(true);
|
||||||
|
@ -112,7 +116,6 @@ export const Terminal = ({
|
||||||
key: "rm",
|
key: "rm",
|
||||||
description: "Remove files or directories",
|
description: "Remove files or directories",
|
||||||
handle: (currentLine, parameters) => {
|
handle: (currentLine, parameters) => {
|
||||||
console.log(parameters);
|
|
||||||
if (parameters.startsWith("-r ")) {
|
if (parameters.startsWith("-r ")) {
|
||||||
const folder = parameters.slice("-r ".length);
|
const folder = parameters.slice("-r ".length);
|
||||||
if (childrenPaths.includes(folder)) {
|
if (childrenPaths.includes(folder)) {
|
||||||
|
@ -246,7 +249,7 @@ export const Terminal = ({
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<textarea
|
<textarea
|
||||||
className="absolute -top-1 -left-6 -right-6 w-screen rounded-none opacity-0"
|
className="absolute -left-6 -right-6 -top-1 w-screen rounded-none opacity-0"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
import { cJoin } from "helpers/className";
|
import { cJoin } from "helpers/className";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src: string;
|
src: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const ColoredSvg = ({ src, className }: Props): JSX.Element => (
|
export const ColoredSvg = ({ src, className }: Props): JSX.Element => (
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`transition-colors ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center]`,
|
`transition-colors ![mask-position:center] ![mask-repeat:no-repeat] ![mask-size:contain]`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{ mask: `url('${src}')`, WebkitMask: `url('${src}')` }}
|
style={{ mask: `url('${src}')`, WebkitMask: `url('${src}')` }}
|
||||||
|
|
|
@ -19,6 +19,12 @@ export enum ContentPanelWidthSizes {
|
||||||
Full = "full",
|
Full = "full",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentPanelWidthSizesToClassName: Record<ContentPanelWidthSizes, string> = {
|
||||||
|
default: "max-w-2xl",
|
||||||
|
large: "max-w-4xl",
|
||||||
|
full: "w-full",
|
||||||
|
};
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const ContentPanel = ({
|
export const ContentPanel = ({
|
||||||
|
@ -31,13 +37,9 @@ export const ContentPanel = ({
|
||||||
<div className="grid h-full">
|
<div className="grid h-full">
|
||||||
<main
|
<main
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"relative justify-self-center px-4 pt-10 pb-20",
|
"relative justify-self-center",
|
||||||
cIf(isContentPanelAtLeast3xl, "px-10 pt-20 pb-32"),
|
cIf(isContentPanelAtLeast3xl, "px-10 pb-32 pt-20", "px-4 pb-20 pt-10"),
|
||||||
width === ContentPanelWidthSizes.Default
|
contentPanelWidthSizesToClassName[width],
|
||||||
? "max-w-2xl"
|
|
||||||
: width === ContentPanelWidthSizes.Large
|
|
||||||
? "max-w-4xl"
|
|
||||||
: "w-full",
|
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
onFocusChanged?: (isFocused: boolean) => void;
|
onFocusChanged?: (isFocused: boolean) => void;
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { Ico } from "components/Ico";
|
import { Ico } from "components/Ico";
|
||||||
import { PageSelector } from "components/Inputs/PageSelector";
|
import { PageSelector } from "components/Inputs/PageSelector";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { isUndefined } from "helpers/asserts";
|
import { isUndefined } from "helpers/asserts";
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
import { useScrollTopOnChange } from "hooks/useScrollOnChange";
|
||||||
import { Ids } from "types/ids";
|
import { Ids } from "types/ids";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: number;
|
page: number;
|
||||||
onPageChange: (newPage: number) => void;
|
onPageChange: (newPage: number) => void;
|
||||||
|
@ -14,6 +20,8 @@ interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const Paginator = ({
|
export const Paginator = ({
|
||||||
page,
|
page,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
|
@ -21,8 +29,14 @@ export const Paginator = ({
|
||||||
children,
|
children,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
useScrollTopOnChange(Ids.ContentPanel, [page]);
|
useScrollTopOnChange(Ids.ContentPanel, [page]);
|
||||||
|
useHotkeys("left", () => onPageChange(page - 1), { enabled: page > 1 }, [page]);
|
||||||
|
useHotkeys("right", () => onPageChange(page + 1), { enabled: page < (totalNumberOfPages ?? 0) }, [
|
||||||
|
page,
|
||||||
|
]);
|
||||||
|
|
||||||
if (totalNumberOfPages === 0) return <DefaultRenderWhenEmpty />;
|
if (totalNumberOfPages === 0) return <DefaultRenderWhenEmpty />;
|
||||||
if (isUndefined(totalNumberOfPages) || totalNumberOfPages < 2) return <>{children}</>;
|
if (isUndefined(totalNumberOfPages) || totalNumberOfPages < 2) return <>{children}</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSelector
|
<PageSelector
|
||||||
|
@ -50,6 +64,7 @@ export const Paginator = ({
|
||||||
const DefaultRenderWhenEmpty = () => {
|
const DefaultRenderWhenEmpty = () => {
|
||||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||||
const { format } = useFormat();
|
const { format } = useFormat();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full place-content-center">
|
<div className="grid h-full place-content-center">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { cIf, cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomSetter } from "helpers/atoms";
|
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||||
import { Button } from "components/Inputs/Button";
|
import { Button } from "components/Inputs/Button";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -11,11 +11,11 @@ import { Button } from "components/Inputs/Button";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
onOpen?: () => void;
|
||||||
onCloseRequest?: () => void;
|
onCloseRequest?: () => void;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
fillViewport?: boolean;
|
fillViewport?: boolean;
|
||||||
hideBackground?: boolean;
|
|
||||||
padding?: boolean;
|
padding?: boolean;
|
||||||
withCloseButton?: boolean;
|
withCloseButton?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,18 @@ interface Props {
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const Popup = ({
|
export const Popup = ({
|
||||||
|
onOpen,
|
||||||
onCloseRequest,
|
onCloseRequest,
|
||||||
isVisible,
|
isVisible,
|
||||||
children,
|
children,
|
||||||
fillViewport,
|
fillViewport,
|
||||||
hideBackground = false,
|
|
||||||
padding = true,
|
padding = true,
|
||||||
withCloseButton = true,
|
withCloseButton = true,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const setMenuGesturesEnabled = useAtomSetter(atoms.layout.menuGesturesEnabled);
|
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]);
|
useHotkeys("escape", () => onCloseRequest?.(), { enabled: isVisible }, [onCloseRequest]);
|
||||||
|
|
||||||
|
@ -39,31 +42,53 @@ export const Popup = ({
|
||||||
setMenuGesturesEnabled(!isVisible);
|
setMenuGesturesEnabled(!isVisible);
|
||||||
}, [isVisible, setMenuGesturesEnabled]);
|
}, [isVisible, setMenuGesturesEnabled]);
|
||||||
|
|
||||||
return (
|
// Used to unload the component if not visible
|
||||||
|
useEffect(() => {
|
||||||
|
const timeouts: NodeJS.Timeout[] = [];
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setActuallyVisible(false);
|
||||||
|
timeouts.push(setTimeout(() => setHidden(true), 600));
|
||||||
|
}
|
||||||
|
return () => timeouts.forEach(clearTimeout);
|
||||||
|
}, [isVisible, onOpen]);
|
||||||
|
|
||||||
|
return isHidden ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"fixed inset-0 z-50 grid place-content-center transition-filter duration-500",
|
"fixed inset-0 z-50 grid place-content-center transition-filter duration-500",
|
||||||
cIf(isVisible, "backdrop-blur", "pointer-events-none touch-none")
|
cIf(!isActuallyVisible, "pointer-events-none touch-none"),
|
||||||
|
cIf(isActuallyVisible && !isPerfModeEnabled, "backdrop-blur")
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"fixed inset-0 bg-shade transition-all duration-500",
|
"fixed inset-0 transition-colors duration-500",
|
||||||
cIf(isVisible, "bg-opacity-50", "bg-opacity-0")
|
cIf(isActuallyVisible, "bg-shade/50", "bg-shade/0")
|
||||||
)}
|
)}
|
||||||
onClick={onCloseRequest}
|
onClick={onCloseRequest}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"grid place-items-center gap-4 transition-transform",
|
`grid place-items-center gap-4 rounded-lg bg-light shadow-2xl transition-transform
|
||||||
|
shadow-shade`,
|
||||||
cIf(padding, "p-10"),
|
cIf(padding, "p-10"),
|
||||||
cIf(isVisible, "scale-100", "scale-0"),
|
cIf(isActuallyVisible, "scale-100", "scale-0"),
|
||||||
cIf(
|
cIf(
|
||||||
fillViewport,
|
fillViewport,
|
||||||
"absolute inset-10 content-start overflow-scroll",
|
"absolute inset-10 content-start overflow-scroll",
|
||||||
"relative max-h-[80vh] overflow-y-auto"
|
"relative max-h-[80vh] overflow-y-auto"
|
||||||
),
|
)
|
||||||
cIf(!hideBackground, "rounded-lg bg-light shadow-2xl shadow-shade")
|
|
||||||
)}>
|
)}>
|
||||||
{withCloseButton && (
|
{withCloseButton && (
|
||||||
<div className="absolute right-6 top-6">
|
<div className="absolute right-6 top-6">
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const SubPanel = ({ children }: Props): JSX.Element => {
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"grid gap-y-2 text-center",
|
"grid gap-y-2 text-center",
|
||||||
cIf(isSubPanelAtLeastXs, "px-10 pt-10 pb-20", "p-4")
|
cIf(isSubPanelAtLeastXs, "px-10 pb-20 pt-10", "p-4")
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { useState } from "react";
|
import { MouseEventHandler, useState } from "react";
|
||||||
import { Link } from "components/Inputs/Link";
|
import { Link } from "components/Inputs/Link";
|
||||||
import { cIf, cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
|
import { atoms } from "contexts/atoms";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -8,29 +15,38 @@ interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
noBackground?: boolean;
|
noBackground?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const UpPressable = ({
|
export const UpPressable = ({
|
||||||
children,
|
children,
|
||||||
href,
|
href,
|
||||||
className,
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
noBackground = false,
|
noBackground = false,
|
||||||
|
onClick,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [isFocused, setFocused] = useState(false);
|
const [isFocused, setFocused] = useState(false);
|
||||||
|
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
onFocusChanged={setFocused}
|
onFocusChanged={setFocused}
|
||||||
|
onClick={onClick}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`drop-shadow-lg transition-all duration-300 shadow-shade`,
|
"transition-all duration-300 !shadow-shade",
|
||||||
|
cIf(isPerfModeEnabled, "shadow-lg", "drop-shadow-lg"),
|
||||||
cIf(!noBackground, "overflow-hidden rounded-md bg-highlight"),
|
cIf(!noBackground, "overflow-hidden rounded-md bg-highlight"),
|
||||||
cIf(
|
cIf(
|
||||||
disabled,
|
disabled,
|
||||||
"cursor-not-allowed opacity-50 grayscale",
|
"cursor-not-allowed opacity-50 grayscale",
|
||||||
cJoin(
|
cJoin(
|
||||||
"cursor-pointer hover:scale-102 hover:drop-shadow-xl",
|
"cursor-pointer hover:scale-102",
|
||||||
cIf(isFocused, "hover:scale-105 hover:drop-shadow-2xl hover:duration-100")
|
cIf(isPerfModeEnabled, "hover:shadow-xl", "hover:drop-shadow-xl"),
|
||||||
|
cIf(isFocused, "hover:scale-105 hover:duration-100")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
className
|
className
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,12 +4,19 @@ import { TranslatedProps } from "types/TranslatedProps";
|
||||||
import { UpPressable } from "components/Containers/UpPressable";
|
import { UpPressable } from "components/Containers/UpPressable";
|
||||||
import { cIf, cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface PreviewFolderProps {
|
interface PreviewFolderProps {
|
||||||
href: string;
|
href: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const PreviewFolder = ({ href, title, disabled }: PreviewFolderProps): JSX.Element => (
|
export const PreviewFolder = ({ href, title, disabled }: PreviewFolderProps): JSX.Element => (
|
||||||
<UpPressable href={href} disabled={disabled}>
|
<UpPressable href={href} disabled={disabled}>
|
||||||
<div
|
<div
|
||||||
|
@ -23,7 +30,10 @@ export const PreviewFolder = ({ href, title, disabled }: PreviewFolderProps): JS
|
||||||
</UpPressable>
|
</UpPressable>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
/*
|
||||||
|
* ╭──────────────────────╮
|
||||||
|
* ───────────────────────────────────╯ TRANSLATED VARIANT ╰──────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
export const TranslatedPreviewFolder = ({
|
export const TranslatedPreviewFolder = ({
|
||||||
translations,
|
translations,
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
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>
|
||||||
|
);
|
|
@ -33,8 +33,8 @@ export const Ico = ({
|
||||||
<span
|
<span
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`material-symbols-rounded [font-size:inherit]
|
`material-symbols-rounded select-none
|
||||||
[line-height:inherit]`,
|
[font-size:inherit] [line-height:inherit]`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
|
|
|
@ -20,12 +20,13 @@ interface Props {
|
||||||
icon?: MaterialSymbol;
|
icon?: MaterialSymbol;
|
||||||
text?: string | null | undefined;
|
text?: string | null | undefined;
|
||||||
alwaysNewTab?: boolean;
|
alwaysNewTab?: boolean;
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
onMouseUp?: MouseEventHandler<HTMLDivElement>;
|
onMouseUp?: MouseEventHandler<HTMLButtonElement>;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
badgeNumber?: number;
|
badgeNumber?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
size?: "normal" | "small";
|
size?: "normal" | "small";
|
||||||
|
type?: "button" | "reset" | "submit";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
@ -43,55 +44,57 @@ export const Button = ({
|
||||||
alwaysNewTab = false,
|
alwaysNewTab = false,
|
||||||
badgeNumber,
|
badgeNumber,
|
||||||
disabled,
|
disabled,
|
||||||
|
type,
|
||||||
size = "normal",
|
size = "normal",
|
||||||
}: Props): JSX.Element => (
|
}: Props): JSX.Element => (
|
||||||
<Link href={href} alwaysNewTab={alwaysNewTab} disabled={disabled}>
|
<Link href={href} alwaysNewTab={alwaysNewTab} disabled={disabled}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<button
|
||||||
|
type={type}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
id={id}
|
id={id}
|
||||||
onClick={(event) => !disabled && onClick?.(event)}
|
disabled={disabled}
|
||||||
|
onClick={(event) => onClick?.(event)}
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onFocus={(event) => event.target.blur()}
|
onFocus={(event) => event.target.blur()}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`group grid cursor-pointer select-none grid-flow-col place-content-center
|
`group grid w-full grid-flow-col
|
||||||
place-items-center gap-2 rounded-full border border-dark py-3 px-4
|
place-content-center place-items-center gap-2 rounded-full border
|
||||||
leading-none text-dark transition-all`,
|
border-dark leading-none text-dark transition-all
|
||||||
cIf(size === "small", "px-3 py-1 text-xs"),
|
disabled:cursor-not-allowed disabled:opacity-50 disabled:grayscale`,
|
||||||
cIf(active, "!border-black bg-black !text-light drop-shadow-lg shadow-black"),
|
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"),
|
||||||
cIf(
|
cIf(
|
||||||
disabled,
|
!disabled && !active,
|
||||||
"cursor-not-allowed opacity-50 grayscale",
|
`shadow-shade hover:bg-dark hover:text-light hover:shadow-lg hover:shadow-shade
|
||||||
cIf(
|
active:hover:!border-black active:hover:bg-black active:hover:!text-light
|
||||||
!active,
|
active:hover:shadow-lg active:hover:shadow-black`
|
||||||
`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
|
className
|
||||||
)}>
|
)}>
|
||||||
{isDefined(badgeNumber) && (
|
{isDefined(badgeNumber) && (
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`absolute -top-3 -right-2 grid h-8 w-8 place-items-center rounded-full bg-dark
|
`absolute grid place-items-center rounded-full bg-dark
|
||||||
font-bold text-light transition-opacity group-hover:opacity-0`,
|
font-bold text-light transition-opacity group-hover:opacity-0`,
|
||||||
cIf(size === "small", "-top-2 -right-2 h-5 w-5")
|
cIf(size === "small", "-right-2 -top-2 h-5 w-5", "-right-2 -top-3 h-8 w-8")
|
||||||
)}>
|
)}>
|
||||||
<p className="-translate-y-[0.05em]">{badgeNumber}</p>
|
<p className="-translate-y-[0.05em]">{badgeNumber}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isDefinedAndNotEmpty(icon) && (
|
{isDefinedAndNotEmpty(icon) && (
|
||||||
<Ico
|
<Ico
|
||||||
className="[font-size:150%] [line-height:0.66]"
|
className="![font-size:150%] ![line-height:0.66]"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
isFilled={active}
|
isFilled={active}
|
||||||
opticalSize={size === "normal" ? 24 : 20}
|
opticalSize={size === "normal" ? 24 : 20}
|
||||||
weight={size === "normal" ? 500 : 800}
|
weight={size === "normal" ? 500 : 800}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>}
|
{isDefinedAndNotEmpty(text) && (
|
||||||
</div>
|
<p className="line-clamp-1 -translate-y-[0.05em] text-center leading-5">{text}</p>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,57 +1,110 @@
|
||||||
|
import type { Placement } from "tippy.js";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { ToolTip } from "components/ToolTip";
|
import { ToolTip } from "components/ToolTip";
|
||||||
import { cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
import { isDefined } from "helpers/asserts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Props {
|
type ButtonProps = Parameters<typeof Button>[0];
|
||||||
|
|
||||||
|
export interface ButtonGroupProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonsProps: (Parameters<typeof Button>[0] & {
|
vertical?: boolean;
|
||||||
tooltip?: string | null | undefined;
|
size?: ButtonProps["size"];
|
||||||
|
buttonsProps: (Omit<ButtonProps, "size"> & {
|
||||||
|
visible?: boolean;
|
||||||
|
tooltip?: React.ReactNode | null | undefined;
|
||||||
|
tooltipPlacement?: Placement;
|
||||||
})[];
|
})[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const ButtonGroup = ({ buttonsProps, className }: Props): JSX.Element => (
|
export const ButtonGroup = ({
|
||||||
<div className={cJoin("grid grid-flow-col", className)}>
|
buttonsProps,
|
||||||
{buttonsProps.map((buttonProps, index) => (
|
className,
|
||||||
<ConditionalWrapper
|
vertical,
|
||||||
key={index}
|
size,
|
||||||
isWrapping={isDefinedAndNotEmpty(buttonProps.tooltip)}
|
}: ButtonGroupProps): JSX.Element => (
|
||||||
wrapper={ToolTipWrapper}
|
<FilteredButtonGroup
|
||||||
wrapperProps={{ text: buttonProps.tooltip ?? "" }}>
|
buttonsProps={buttonsProps.filter((button) => button.visible !== false)}
|
||||||
<Button
|
className={className}
|
||||||
{...buttonProps}
|
vertical={vertical}
|
||||||
className={
|
size={size}
|
||||||
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 ╰──────────────────────────────────────
|
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface ToolTipWrapperProps {
|
interface ToolTipWrapperProps {
|
||||||
text: string;
|
text: React.ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolTipWrapper = ({ text, children }: ToolTipWrapperProps & Wrapper) => (
|
const ToolTipWrapper = ({ text, children, placement }: ToolTipWrapperProps & Wrapper) => (
|
||||||
<ToolTip content={text}>
|
<ToolTip content={text} placement={placement}>
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,11 +2,9 @@ import { Fragment } from "react";
|
||||||
import { ToolTip } from "../ToolTip";
|
import { ToolTip } from "../ToolTip";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { cJoin } from "helpers/className";
|
import { cJoin } from "helpers/className";
|
||||||
import { prettyLanguage } from "helpers/formatters";
|
|
||||||
import { iterateMap } from "helpers/others";
|
import { iterateMap } from "helpers/others";
|
||||||
import { sendAnalytics } from "helpers/analytics";
|
import { sendAnalytics } from "helpers/analytics";
|
||||||
import { atoms } from "contexts/atoms";
|
import { useFormat } from "hooks/useFormat";
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -32,7 +30,7 @@ export const LanguageSwitcher = ({
|
||||||
onLanguageChanged,
|
onLanguageChanged,
|
||||||
showBadge = true,
|
showBadge = true,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const languages = useAtomGetter(atoms.localData.languages);
|
const { formatLanguage } = useFormat();
|
||||||
return (
|
return (
|
||||||
<ToolTip
|
<ToolTip
|
||||||
content={
|
content={
|
||||||
|
@ -45,7 +43,7 @@ export const LanguageSwitcher = ({
|
||||||
onLanguageChanged(value);
|
onLanguageChanged(value);
|
||||||
sendAnalytics("Language Switcher", `Switch language (${locale})`);
|
sendAnalytics("Language Switcher", `Switch language (${locale})`);
|
||||||
}}
|
}}
|
||||||
text={prettyLanguage(locale, languages)}
|
text={formatLanguage(locale)}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -4,17 +4,24 @@ import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||||
import { cIf, cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
href: string | null | undefined;
|
href: string | null | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
alwaysNewTab?: boolean;
|
alwaysNewTab?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
onFocusChanged?: (isFocused: boolean) => void;
|
onFocusChanged?: (isFocused: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
linkStyled?: boolean;
|
linkStyled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const Link = ({
|
export const Link = ({
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
|
@ -22,6 +29,7 @@ export const Link = ({
|
||||||
alwaysNewTab,
|
alwaysNewTab,
|
||||||
disabled,
|
disabled,
|
||||||
linkStyled = false,
|
linkStyled = false,
|
||||||
|
onClick,
|
||||||
onFocusChanged,
|
onFocusChanged,
|
||||||
}: Props): JSX.Element => (
|
}: Props): JSX.Element => (
|
||||||
<ConditionalWrapper
|
<ConditionalWrapper
|
||||||
|
@ -29,6 +37,7 @@ export const Link = ({
|
||||||
wrapperProps={{
|
wrapperProps={{
|
||||||
href: href ?? "",
|
href: href ?? "",
|
||||||
alwaysNewTab,
|
alwaysNewTab,
|
||||||
|
onClick,
|
||||||
onFocusChanged,
|
onFocusChanged,
|
||||||
className: cJoin(
|
className: cJoin(
|
||||||
cIf(
|
cIf(
|
||||||
|
@ -46,17 +55,24 @@ export const Link = ({
|
||||||
</ConditionalWrapper>
|
</ConditionalWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭──────────────────────╮
|
||||||
|
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface LinkWrapperProps {
|
interface LinkWrapperProps {
|
||||||
href: string;
|
href: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
alwaysNewTab?: boolean;
|
alwaysNewTab?: boolean;
|
||||||
onFocusChanged?: (isFocused: boolean) => void;
|
onFocusChanged?: (isFocused: boolean) => void;
|
||||||
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkWrapper = ({
|
const LinkWrapper = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
onFocusChanged,
|
onFocusChanged,
|
||||||
|
onClick,
|
||||||
alwaysNewTab = false,
|
alwaysNewTab = false,
|
||||||
href,
|
href,
|
||||||
}: LinkWrapperProps & Wrapper) => (
|
}: LinkWrapperProps & Wrapper) => (
|
||||||
|
@ -65,6 +81,7 @@ const LinkWrapper = ({
|
||||||
className={className}
|
className={className}
|
||||||
target={alwaysNewTab ? "_blank" : "_self"}
|
target={alwaysNewTab ? "_blank" : "_self"}
|
||||||
replace={href.startsWith("#")}
|
replace={href.startsWith("#")}
|
||||||
|
onClick={onClick}
|
||||||
onMouseLeave={() => onFocusChanged?.(false)}
|
onMouseLeave={() => onFocusChanged?.(false)}
|
||||||
onMouseDown={() => onFocusChanged?.(true)}
|
onMouseDown={() => onFocusChanged?.(true)}
|
||||||
onMouseUp={() => onFocusChanged?.(false)}>
|
onMouseUp={() => onFocusChanged?.(false)}>
|
||||||
|
@ -72,6 +89,8 @@ const LinkWrapper = ({
|
||||||
</NextLink>
|
</NextLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
interface DisabledWrapperProps {
|
interface DisabledWrapperProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ const InsertedLabel = ({ label }: InsertedLabelProps) => (
|
||||||
export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Element => {
|
export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Element => {
|
||||||
const updateOrder = useCallback(
|
const updateOrder = useCallback(
|
||||||
(sourceIndex: number, targetIndex: number) => {
|
(sourceIndex: number, targetIndex: number) => {
|
||||||
console.log("updateOrder");
|
|
||||||
onChange?.(arrayMove(items, sourceIndex, targetIndex));
|
onChange?.(arrayMove(items, sourceIndex, targetIndex));
|
||||||
},
|
},
|
||||||
[items, onChange]
|
[items, onChange]
|
||||||
|
|
|
@ -52,19 +52,18 @@ export const Select = ({
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"relative text-center transition-filter",
|
"relative text-center transition-filter",
|
||||||
cIf(isOpened, "z-10 drop-shadow-lg shadow-shade"),
|
cIf(isOpened, "z-20 drop-shadow-lg shadow-shade"),
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`grid cursor-pointer select-none grid-flow-col grid-cols-[1fr_auto_auto]
|
`grid cursor-pointer select-none grid-flow-col grid-cols-[1fr_auto_auto]
|
||||||
place-items-center rounded-3xl p-1 outline outline-1 -outline-offset-1
|
place-items-center rounded-3xl p-1 outline outline-1 -outline-offset-1`,
|
||||||
outline-mid`,
|
|
||||||
cIf(isOpened, "rounded-b-none bg-highlight outline-transparent"),
|
cIf(isOpened, "rounded-b-none bg-highlight outline-transparent"),
|
||||||
cIf(
|
cIf(
|
||||||
disabled,
|
disabled,
|
||||||
"cursor-not-allowed text-dark opacity-50 outline-dark/60 grayscale",
|
"cursor-not-allowed text-dark opacity-50 outline-dark/60 grayscale",
|
||||||
"transition-all hover:bg-mid hover:outline-transparent"
|
"outline-mid transition-all hover:bg-mid hover:outline-transparent"
|
||||||
)
|
)
|
||||||
)}>
|
)}>
|
||||||
<p onClick={tryToggling} className="w-full px-4 py-1">
|
<p onClick={tryToggling} className="w-full px-4 py-1">
|
||||||
|
|
|
@ -21,10 +21,14 @@ export const Switch = ({ value, onClick, className, disabled = false }: Props):
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
`relative grid h-6 w-12 content-center rounded-full border-mid outline
|
`relative grid h-6 w-12 content-center rounded-full border-mid outline
|
||||||
outline-1 -outline-offset-1 outline-mid transition-colors`,
|
outline-1 -outline-offset-1 transition-colors`,
|
||||||
cIf(value, "border-none bg-mid shadow-inner-sm outline-transparent shadow-shade"),
|
cIf(value, "border-none shadow-inner-sm shadow-shade"),
|
||||||
cIf(disabled, "cursor-not-allowed opacity-50 grayscale", "cursor-pointer"),
|
cIf(disabled, "cursor-not-allowed opacity-50 grayscale", "cursor-pointer outline-mid"),
|
||||||
cIf(disabled, cIf(value, "bg-dark/40 outline-transparent", "outline-dark/60")),
|
cIf(
|
||||||
|
disabled,
|
||||||
|
cIf(value, "bg-dark/40 outline-transparent", "outline-dark/60"),
|
||||||
|
cIf(value, "bg-mid outline-transparent")
|
||||||
|
),
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { forwardRef } from "react";
|
||||||
import { Ico } from "components/Ico";
|
import { Ico } from "components/Ico";
|
||||||
import { cIf, cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||||
|
@ -18,34 +19,32 @@ interface Props {
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const TextInput = ({
|
export const TextInput = forwardRef<HTMLInputElement, Props>(
|
||||||
value,
|
({ value, onChange, className, name, placeholder, disabled = false }, ref) => (
|
||||||
onChange,
|
<div className={cJoin("relative", className)}>
|
||||||
className,
|
<input
|
||||||
name,
|
ref={ref}
|
||||||
placeholder,
|
className="w-full"
|
||||||
disabled = false,
|
type="text"
|
||||||
}: Props): JSX.Element => (
|
name={name}
|
||||||
<div className={cJoin("relative", className)}>
|
autoCapitalize="off"
|
||||||
<input
|
value={value}
|
||||||
className="w-full"
|
disabled={disabled}
|
||||||
type="text"
|
placeholder={placeholder ?? undefined}
|
||||||
name={name}
|
onChange={(event) => {
|
||||||
value={value}
|
onChange(event.target.value);
|
||||||
disabled={disabled}
|
}}
|
||||||
placeholder={placeholder ?? undefined}
|
/>
|
||||||
onChange={(event) => {
|
{isDefinedAndNotEmpty(value) && (
|
||||||
onChange(event.target.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"))}
|
||||||
{isDefinedAndNotEmpty(value) && (
|
icon="close"
|
||||||
<div className="absolute right-4 top-0 bottom-0 grid place-items-center">
|
onClick={() => !disabled && onChange("")}
|
||||||
<Ico
|
/>
|
||||||
className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))}
|
</div>
|
||||||
icon="close"
|
)}
|
||||||
onClick={() => !disabled && onChange("")}
|
</div>
|
||||||
/>
|
)
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
TextInput.displayName = "TextInput";
|
||||||
|
|
|
@ -12,6 +12,11 @@ import { isDefined } from "helpers/asserts";
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
@ -34,6 +39,7 @@ export const LightBox = ({
|
||||||
onPressNext,
|
onPressNext,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [currentZoom, setCurrentZoom] = useState(1);
|
const [currentZoom, setCurrentZoom] = useState(1);
|
||||||
|
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||||
const { isFullscreen, toggleFullscreen, exitFullscreen, requestFullscreen } = useFullscreen(
|
const { isFullscreen, toggleFullscreen, exitFullscreen, requestFullscreen } = useFullscreen(
|
||||||
Ids.LightBox
|
Ids.LightBox
|
||||||
);
|
);
|
||||||
|
@ -57,12 +63,12 @@ export const LightBox = ({
|
||||||
id={Ids.LightBox}
|
id={Ids.LightBox}
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"fixed inset-0 z-50 grid place-content-center transition-filter duration-500",
|
"fixed inset-0 z-50 grid place-content-center transition-filter duration-500",
|
||||||
cIf(isVisible, "backdrop-blur", "pointer-events-none touch-none")
|
cIf(isVisible, cIf(!isPerfModeEnabled, "backdrop-blur"), "pointer-events-none touch-none")
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"fixed inset-0 bg-shade transition-all duration-500",
|
"fixed inset-0 transition-colors duration-500",
|
||||||
cIf(isVisible, "bg-opacity-50", "bg-opacity-0")
|
cIf(isVisible, "bg-shade/50", "bg-shade/0")
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
@ -85,8 +91,10 @@ export const LightBox = ({
|
||||||
}}>
|
}}>
|
||||||
{isDefined(src) && (
|
{isDefined(src) && (
|
||||||
<Img
|
<Img
|
||||||
className={`h-[calc(100vh-4rem)] w-full object-contain drop-shadow-2xl
|
className={cJoin(
|
||||||
shadow-shade`}
|
`h-[calc(100vh-4rem)] w-full object-contain`,
|
||||||
|
cIf(!isPerfModeEnabled, "drop-shadow-2xl shadow-shade")
|
||||||
|
)}
|
||||||
src={src}
|
src={src}
|
||||||
quality={ImageQuality.Large}
|
quality={ImageQuality.Large}
|
||||||
/>
|
/>
|
||||||
|
@ -119,6 +127,11 @@ export const LightBox = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭──────────────────────╮
|
||||||
|
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface ControlButtonsProps {
|
interface ControlButtonsProps {
|
||||||
isPreviousImageAvailable: boolean;
|
isPreviousImageAvailable: boolean;
|
||||||
isNextImageAvailable: boolean;
|
isNextImageAvailable: boolean;
|
||||||
|
@ -162,7 +175,7 @@ const ControlButtons = ({
|
||||||
<NextButton />
|
<NextButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-2 right-2 grid gap-4 rounded-4xl p-4 backdrop-blur-lg">
|
<div className="absolute right-2 top-2 grid gap-4 rounded-4xl p-4 backdrop-blur-lg">
|
||||||
<CloseButton />
|
<CloseButton />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -170,20 +183,20 @@ const ControlButtons = ({
|
||||||
<>
|
<>
|
||||||
{isPreviousImageAvailable && (
|
{isPreviousImageAvailable && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 left-8 grid gap-4 rounded-4xl p-4
|
className={`absolute left-8 top-1/2 grid gap-4 rounded-4xl p-4
|
||||||
backdrop-blur-lg`}>
|
backdrop-blur-lg`}>
|
||||||
<PreviousButton />
|
<PreviousButton />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isNextImageAvailable && (
|
{isNextImageAvailable && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 right-8 grid gap-4 rounded-4xl p-4
|
className={`absolute right-8 top-1/2 grid gap-4 rounded-4xl p-4
|
||||||
backdrop-blur-lg`}>
|
backdrop-blur-lg`}>
|
||||||
<NextButton />
|
<NextButton />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`absolute top-4 right-8 grid gap-4 rounded-4xl p-4
|
className={`absolute right-8 top-4 grid gap-4 rounded-4xl p-4
|
||||||
backdrop-blur-lg`}>
|
backdrop-blur-lg`}>
|
||||||
<CloseButton />
|
<CloseButton />
|
||||||
<FullscreenButton />
|
<FullscreenButton />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Markdown from "markdown-to-jsx";
|
import Markdown from "markdown-to-jsx";
|
||||||
import React, { Fragment, useMemo } from "react";
|
import React, { Fragment, MouseEventHandler, useMemo } from "react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
|
import { z } from "zod";
|
||||||
import { HorizontalLine } from "components/HorizontalLine";
|
import { HorizontalLine } from "components/HorizontalLine";
|
||||||
import { Img } from "components/Img";
|
import { Img } from "components/Img";
|
||||||
import { InsetBox } from "components/Containers/InsetBox";
|
import { InsetBox } from "components/Containers/InsetBox";
|
||||||
|
@ -16,6 +17,8 @@ import { atoms } from "contexts/atoms";
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
import { Link } from "components/Inputs/Link";
|
import { Link } from "components/Inputs/Link";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
import { VideoPlayer } from "components/Player";
|
||||||
|
import { getVideoFile } from "helpers/videos";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -117,14 +120,43 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
||||||
},
|
},
|
||||||
|
|
||||||
Line: {
|
Line: {
|
||||||
component: (compProps) => (
|
component: (compProps) => {
|
||||||
<>
|
const schema = z.object({ name: z.string(), children: z.any() });
|
||||||
<strong
|
if (!schema.safeParse(compProps).success) {
|
||||||
className={cJoin("!my-0 text-dark/60", cIf(!isContentPanelAtLeastLg, "!-mb-4"))}>
|
return (
|
||||||
<Markdawn text={compProps.name} />
|
<MarkdawnError
|
||||||
</strong>
|
message={`Error while parsing a <Line/> tag. Here is the correct usage:
|
||||||
<p className="whitespace-pre-line">{compProps.children}</p>
|
<Line name="John">Hello!</Line>`}
|
||||||
</>
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const safeProps: z.infer<typeof schema> = compProps;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<strong
|
||||||
|
className={cJoin(
|
||||||
|
"!my-0 text-dark/60",
|
||||||
|
cIf(!isContentPanelAtLeastLg, "!-mb-4")
|
||||||
|
)}>
|
||||||
|
<Markdawn text={safeProps.name} />
|
||||||
|
</strong>
|
||||||
|
<p className="whitespace-pre-line">{safeProps.children}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Angelic: {
|
||||||
|
component: (comProps) => <span className="font-angelic">{comProps.children}</span>,
|
||||||
|
},
|
||||||
|
|
||||||
|
Video: {
|
||||||
|
component: (comProps) => (
|
||||||
|
<VideoPlayer
|
||||||
|
src={getVideoFile(comProps.id)}
|
||||||
|
title={comProps.title}
|
||||||
|
className="my-8"
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -186,7 +218,7 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
||||||
name?: string;
|
name?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className="mt-8 mb-12 grid cursor-pointer place-content-center"
|
className="mb-12 mt-8 grid cursor-pointer place-content-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showLightBox([
|
showLightBox([
|
||||||
compProps.src.startsWith("/uploads/")
|
compProps.src.startsWith("/uploads/")
|
||||||
|
@ -215,35 +247,47 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
interface TableOfContentsProps {
|
interface MarkdawnErrorProps {
|
||||||
text: string;
|
message: string;
|
||||||
title?: string;
|
|
||||||
horizontalLine?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableOfContents = ({
|
const MarkdawnError = ({ message }: MarkdawnErrorProps): JSX.Element => (
|
||||||
text,
|
<div
|
||||||
title,
|
className="flex place-items-center gap-4 whitespace-pre-line rounded-md
|
||||||
horizontalLine = false,
|
bg-[red]/10 px-4 text-[red]">
|
||||||
}: TableOfContentsProps): JSX.Element => {
|
<Ico icon="error" isFilled={false} />
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
interface TableOfContentsProps {
|
||||||
|
toc: TocInterface;
|
||||||
|
onContentClicked?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableOfContents = ({ toc, onContentClicked }: TableOfContentsProps): JSX.Element => {
|
||||||
const { format } = useFormat();
|
const { format } = useFormat();
|
||||||
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toc.children.length > 0 && (
|
{toc.children.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{horizontalLine && <HorizontalLine />}
|
|
||||||
<h3 className="text-xl">{format("table_of_contents")}</h3>
|
<h3 className="text-xl">{format("table_of_contents")}</h3>
|
||||||
<div className="max-w-[14.5rem] text-left">
|
<div className="max-w-[14.5rem] text-left">
|
||||||
<p
|
<p
|
||||||
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
|
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
|
||||||
text-left">
|
text-left">
|
||||||
<Link href={`#${toc.slug}`} linkStyled>
|
<Link href={`#${toc.slug}`} linkStyled onClick={onContentClicked}>
|
||||||
{<abbr title={toc.title}>{toc.title}</abbr>}
|
{<abbr title={toc.title}>{toc.title}</abbr>}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<TocLevel tocchildren={toc.children} parentNumbering="" />
|
<TocLevel
|
||||||
|
tocchildren={toc.children}
|
||||||
|
parentNumbering=""
|
||||||
|
onContentClicked={onContentClicked}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -268,7 +312,7 @@ const Header = ({ level, title, slug }: HeaderProps): JSX.Element => {
|
||||||
<>
|
<>
|
||||||
<div className="ml-10 flex place-items-center gap-4">
|
<div className="ml-10 flex place-items-center gap-4">
|
||||||
{title === "* * *" ? (
|
{title === "* * *" ? (
|
||||||
<div className="mt-8 mb-12 space-x-3 text-dark">
|
<div className="mb-12 mt-8 space-x-3 text-dark">
|
||||||
<Ico icon="emergency" />
|
<Ico icon="emergency" />
|
||||||
<Ico icon="emergency" />
|
<Ico icon="emergency" />
|
||||||
<Ico icon="emergency" />
|
<Ico icon="emergency" />
|
||||||
|
@ -334,12 +378,14 @@ interface LevelProps {
|
||||||
tocchildren: TocInterface[];
|
tocchildren: TocInterface[];
|
||||||
parentNumbering: string;
|
parentNumbering: string;
|
||||||
allowIntersection?: boolean;
|
allowIntersection?: boolean;
|
||||||
|
onContentClicked?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TocLevel = ({
|
const TocLevel = ({
|
||||||
tocchildren,
|
tocchildren,
|
||||||
parentNumbering,
|
parentNumbering,
|
||||||
allowIntersection = true,
|
allowIntersection = true,
|
||||||
|
onContentClicked,
|
||||||
}: LevelProps): JSX.Element => {
|
}: LevelProps): JSX.Element => {
|
||||||
const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
|
const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
|
||||||
const currentIntersection = useIntersectionList(ids);
|
const currentIntersection = useIntersectionList(ids);
|
||||||
|
@ -354,7 +400,7 @@ const TocLevel = ({
|
||||||
cIf(allowIntersection && currentIntersection === childIndex, "text-dark")
|
cIf(allowIntersection && currentIntersection === childIndex, "text-dark")
|
||||||
)}>
|
)}>
|
||||||
<span className="text-dark">{`${parentNumbering}${childIndex + 1}.`}</span>{" "}
|
<span className="text-dark">{`${parentNumbering}${childIndex + 1}.`}</span>{" "}
|
||||||
<Link href={`#${child.slug}`} linkStyled>
|
<Link href={`#${child.slug}`} linkStyled onClick={onContentClicked}>
|
||||||
{<abbr title={child.title}>{child.title}</abbr>}
|
{<abbr title={child.title}>{child.title}</abbr>}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -362,6 +408,7 @@ const TocLevel = ({
|
||||||
tocchildren={child.children}
|
tocchildren={child.children}
|
||||||
parentNumbering={`${parentNumbering}${childIndex + 1}.`}
|
parentNumbering={`${parentNumbering}${childIndex + 1}.`}
|
||||||
allowIntersection={allowIntersection && currentIntersection === childIndex}
|
allowIntersection={allowIntersection && currentIntersection === childIndex}
|
||||||
|
onContentClicked={onContentClicked}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
@ -429,7 +476,14 @@ const markdawnHeadersParser = (
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
export const getTocFromMarkdawn = (
|
||||||
|
markdawn: string | null | undefined,
|
||||||
|
title?: string
|
||||||
|
): TocInterface | undefined => {
|
||||||
|
if (isUndefined(markdawn)) return undefined;
|
||||||
|
|
||||||
|
const text = preprocessMarkDawn(markdawn);
|
||||||
|
|
||||||
const toc: TocInterface = {
|
const toc: TocInterface = {
|
||||||
title: title ?? "Return to top",
|
title: title ?? "Return to top",
|
||||||
slug: slugify(title),
|
slug: slugify(title),
|
||||||
|
@ -516,5 +570,6 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (toc.children.length === 0) return undefined;
|
||||||
return toc;
|
return toc;
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,7 @@ interface Props {
|
||||||
reduced?: boolean;
|
reduced?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
|
@ -2,10 +2,8 @@ import { useCallback } from "react";
|
||||||
import { Button } from "components/Inputs/Button";
|
import { Button } from "components/Inputs/Button";
|
||||||
import { TranslatedProps } from "types/TranslatedProps";
|
import { TranslatedProps } from "types/TranslatedProps";
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
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 { useFormat } from "hooks/useFormat";
|
||||||
|
import { cJoin } from "helpers/className";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -15,27 +13,18 @@ import { useFormat } from "hooks/useFormat";
|
||||||
interface Props {
|
interface Props {
|
||||||
href: string;
|
href: string;
|
||||||
title: string | null | undefined;
|
title: string | null | undefined;
|
||||||
|
|
||||||
displayOnlyOn?: "1ColumnLayout" | "3ColumnsLayout";
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props): JSX.Element => {
|
export const ReturnButton = ({ href, title, className }: Props): JSX.Element => {
|
||||||
const { format } = useFormat();
|
const { format } = useFormat();
|
||||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={cJoin("mx-auto w-full max-w-lg place-self-center", className)}>
|
||||||
{((is3ColumnsLayout && displayOnlyOn === "3ColumnsLayout") ||
|
<Button href={href} text={format("return_to_x", { x: title })} icon="navigate_before" />
|
||||||
(!is3ColumnsLayout && displayOnlyOn === "1ColumnLayout") ||
|
</div>
|
||||||
isUndefined(displayOnlyOn)) && (
|
|
||||||
<div className={className}>
|
|
||||||
<Button href={href} text={format("return_to_x", { x: title })} icon="navigate_before" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
import { HorizontalLine } from "components/HorizontalLine";
|
import { HorizontalLine } from "components/HorizontalLine";
|
||||||
import { Button } from "components/Inputs/Button";
|
import { Button } from "components/Inputs/Button";
|
||||||
import { NavOption } from "components/PanelComponents/NavOption";
|
import { NavOption } from "components/PanelComponents/NavOption";
|
||||||
|
@ -21,8 +22,13 @@ export const MainPanel = (): JSX.Element => {
|
||||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||||
const { format } = useFormat();
|
const { format } = useFormat();
|
||||||
const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced);
|
const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced);
|
||||||
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
|
const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened);
|
||||||
const setSearchOpened = useAtomSetter(atoms.layout.searchOpened);
|
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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -53,7 +59,10 @@ export const MainPanel = (): JSX.Element => {
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="grid place-items-center">
|
<div className="grid place-items-center">
|
||||||
<Link href="/" className="flex w-full cursor-pointer justify-center">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex w-full cursor-pointer justify-center"
|
||||||
|
onClick={closeMainPanel}>
|
||||||
<ColoredSvg
|
<ColoredSvg
|
||||||
src="/icons/accords.svg"
|
src="/icons/accords.svg"
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
|
@ -76,7 +85,9 @@ export const MainPanel = (): JSX.Element => {
|
||||||
content={<h3 className="text-2xl">{format("open_settings")}</h3>}
|
content={<h3 className="text-2xl">{format("open_settings")}</h3>}
|
||||||
placement={isMainPanelReduced ? "right" : "top"}>
|
placement={isMainPanelReduced ? "right" : "top"}>
|
||||||
<Button
|
<Button
|
||||||
|
active={isSettingsOpened}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
closeMainPanel();
|
||||||
setSettingsOpened(true);
|
setSettingsOpened(true);
|
||||||
sendAnalytics("Settings", "Open settings");
|
sendAnalytics("Settings", "Open settings");
|
||||||
}}
|
}}
|
||||||
|
@ -87,13 +98,30 @@ export const MainPanel = (): JSX.Element => {
|
||||||
content={<h3 className="text-2xl">{format("open_search")}</h3>}
|
content={<h3 className="text-2xl">{format("open_search")}</h3>}
|
||||||
placement={isMainPanelReduced ? "right" : "top"}>
|
placement={isMainPanelReduced ? "right" : "top"}>
|
||||||
<Button
|
<Button
|
||||||
|
active={isSearchOpened}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
closeMainPanel();
|
||||||
setSearchOpened(true);
|
setSearchOpened(true);
|
||||||
sendAnalytics("Search", "Open search");
|
sendAnalytics("Search", "Open search");
|
||||||
}}
|
}}
|
||||||
icon="search"
|
icon="search"
|
||||||
/>
|
/>
|
||||||
</ToolTip>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,6 +134,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
title={format("library")}
|
title={format("library")}
|
||||||
subtitle={format("library_short_description")}
|
subtitle={format("library_short_description")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavOption
|
<NavOption
|
||||||
|
@ -114,6 +143,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
title={format("contents")}
|
title={format("contents")}
|
||||||
subtitle={format("contents_short_description")}
|
subtitle={format("contents_short_description")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavOption
|
<NavOption
|
||||||
|
@ -122,6 +152,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
title={format("wiki")}
|
title={format("wiki")}
|
||||||
subtitle={format("wiki_short_description")}
|
subtitle={format("wiki_short_description")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavOption
|
<NavOption
|
||||||
|
@ -130,6 +161,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
title={format("chronicles")}
|
title={format("chronicles")}
|
||||||
subtitle={format("chronicles_short_description")}
|
subtitle={format("chronicles_short_description")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
|
@ -139,22 +171,15 @@ export const MainPanel = (): JSX.Element => {
|
||||||
icon="newspaper"
|
icon="newspaper"
|
||||||
title={format("news")}
|
title={format("news")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
|
||||||
<NavOption
|
|
||||||
url="/merch"
|
|
||||||
icon="store"
|
|
||||||
title={format("merch")}
|
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
|
||||||
/>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<NavOption
|
<NavOption
|
||||||
url="https://gallery.accords-library.com/posts/"
|
url="https://gallery.accords-library.com/posts/"
|
||||||
icon="perm_media"
|
icon="perm_media"
|
||||||
title={format("gallery")}
|
title={format("gallery")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavOption
|
<NavOption
|
||||||
|
@ -162,6 +187,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
icon="save"
|
icon="save"
|
||||||
title={format("archives")}
|
title={format("archives")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavOption
|
<NavOption
|
||||||
|
@ -169,6 +195,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
icon="info"
|
icon="info"
|
||||||
title={format("about_us")}
|
title={format("about_us")}
|
||||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||||
|
onClick={closeMainPanel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(!isMainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />}
|
{(!isMainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />}
|
||||||
|
@ -179,7 +206,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
<Markdawn text={format("licensing_notice")} />
|
<Markdawn text={format("licensing_notice")} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 mb-8 grid place-content-center">
|
<div className="mb-8 mt-4 grid place-content-center">
|
||||||
<Link
|
<Link
|
||||||
onClick={() => sendAnalytics("MainPanel", "Visit license")}
|
onClick={() => sendAnalytics("MainPanel", "Visit license")}
|
||||||
aria-label="Read more about the license we use for this website"
|
aria-label="Read more about the license we use for this website"
|
||||||
|
@ -205,7 +232,7 @@ export const MainPanel = (): JSX.Element => {
|
||||||
<Markdawn text={format("copyright_notice")} />
|
<Markdawn text={format("copyright_notice")} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
|
<div className="mb-4 mt-12 grid h-4 grid-flow-col place-content-center gap-8">
|
||||||
<Link
|
<Link
|
||||||
aria-label="Browse our GitHub repository, which include this website source code"
|
aria-label="Browse our GitHub repository, which include this website source code"
|
||||||
onClick={() => sendAnalytics("MainPanel", "Visit GitHub")}
|
onClick={() => sendAnalytics("MainPanel", "Visit GitHub")}
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { MaterialSymbol } from "material-symbols";
|
import { MaterialSymbol } from "material-symbols";
|
||||||
import { Popup } from "components/Containers/Popup";
|
import { Popup } from "components/Containers/Popup";
|
||||||
import { sendAnalytics } from "helpers/analytics";
|
import { sendAnalytics } from "helpers/analytics";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomPair } from "helpers/atoms";
|
import { useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||||
import { TextInput } from "components/Inputs/TextInput";
|
import { TextInput } from "components/Inputs/TextInput";
|
||||||
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search";
|
import {
|
||||||
|
containsHighlight,
|
||||||
|
CustomSearchResponse,
|
||||||
|
filterHitsWithHighlight,
|
||||||
|
meiliMultiSearch,
|
||||||
|
} from "helpers/search";
|
||||||
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
|
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
|
||||||
import { filterDefined, filterHasAttributes, isDefined } from "helpers/asserts";
|
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||||
import {
|
import {
|
||||||
MeiliContent,
|
MeiliContent,
|
||||||
MeiliIndices,
|
MeiliIndices,
|
||||||
MeiliLibraryItem,
|
MeiliLibraryItem,
|
||||||
MeiliPost,
|
MeiliPost,
|
||||||
MeiliVideo,
|
MeiliVideo,
|
||||||
|
MeiliWeapon,
|
||||||
MeiliWikiPage,
|
MeiliWikiPage,
|
||||||
} from "shared/meilisearch-graphql-typings/meiliTypes";
|
} from "shared/meilisearch-graphql-typings/meiliTypes";
|
||||||
import { getVideoThumbnailURL } from "helpers/videos";
|
import { getVideoThumbnailURL } from "helpers/videos";
|
||||||
import { UpPressable } from "components/Containers/UpPressable";
|
import { UpPressable } from "components/Containers/UpPressable";
|
||||||
import { prettyItemSubType, prettySlug } from "helpers/formatters";
|
import { prettySlug } from "helpers/formatters";
|
||||||
import { Ico } from "components/Ico";
|
import { Ico } from "components/Ico";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
|
||||||
|
@ -34,140 +40,163 @@ const SEARCH_LIMIT = 8;
|
||||||
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
interface MultiResult {
|
||||||
|
libraryItems?: CustomSearchResponse<MeiliLibraryItem>;
|
||||||
|
contents?: CustomSearchResponse<MeiliContent>;
|
||||||
|
videos?: CustomSearchResponse<MeiliVideo>;
|
||||||
|
posts?: CustomSearchResponse<MeiliPost>;
|
||||||
|
wikiPages?: CustomSearchResponse<MeiliWikiPage>;
|
||||||
|
weapons?: CustomSearchResponse<MeiliWeapon>;
|
||||||
|
}
|
||||||
|
|
||||||
export const SearchPopup = (): JSX.Element => {
|
export const SearchPopup = (): JSX.Element => {
|
||||||
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
|
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const { format } = useFormat();
|
const {
|
||||||
const [libraryItems, setLibraryItems] = useState<CustomSearchResponse<MeiliLibraryItem>>();
|
format,
|
||||||
const [contents, setContents] = useState<CustomSearchResponse<MeiliContent>>();
|
formatCategory,
|
||||||
const [videos, setVideos] = useState<CustomSearchResponse<MeiliVideo>>();
|
formatContentType,
|
||||||
const [posts, setPosts] = useState<CustomSearchResponse<MeiliPost>>();
|
formatWikiTag,
|
||||||
const [wikiPages, setWikiPages] = useState<CustomSearchResponse<MeiliWikiPage>>();
|
formatLibraryItemSubType,
|
||||||
|
formatWeaponType,
|
||||||
|
} = useFormat();
|
||||||
|
const [multiResult, setMultiResult] = useState<MultiResult>({});
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchSearchResults = useCallback((q: string) => {
|
||||||
const fetchLibraryItems = async () => {
|
const fetchMultiResult = async () => {
|
||||||
const searchResult = await meiliSearch(MeiliIndices.LIBRARY_ITEM, query, {
|
const searchResults = (
|
||||||
limit: SEARCH_LIMIT,
|
await meiliMultiSearch([
|
||||||
attributesToRetrieve: [
|
{
|
||||||
"title",
|
indexUid: MeiliIndices.LIBRARY_ITEM,
|
||||||
"subtitle",
|
q,
|
||||||
"descriptions",
|
limit: SEARCH_LIMIT,
|
||||||
"id",
|
attributesToRetrieve: [
|
||||||
"slug",
|
"title",
|
||||||
"thumbnail",
|
"subtitle",
|
||||||
"release_date",
|
"descriptions",
|
||||||
"price",
|
"id",
|
||||||
"categories",
|
"slug",
|
||||||
"metadata",
|
"thumbnail",
|
||||||
],
|
"release_date",
|
||||||
attributesToHighlight: ["title", "subtitle", "descriptions"],
|
"price",
|
||||||
attributesToCrop: ["descriptions"],
|
"categories",
|
||||||
});
|
"metadata",
|
||||||
searchResult.hits = searchResult.hits.map((item) => {
|
],
|
||||||
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("descriptions"))) {
|
attributesToHighlight: ["title", "subtitle", "descriptions"],
|
||||||
item._formatted.descriptions = filterDefined(item._formatted.descriptions).filter(
|
attributesToCrop: ["descriptions"],
|
||||||
(description) => containsHighlight(JSON.stringify(description))
|
},
|
||||||
);
|
{
|
||||||
|
indexUid: MeiliIndices.CONTENT,
|
||||||
|
q,
|
||||||
|
limit: SEARCH_LIMIT,
|
||||||
|
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
|
||||||
|
attributesToHighlight: ["translations"],
|
||||||
|
attributesToCrop: ["translations.displayable_description"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indexUid: MeiliIndices.VIDEOS,
|
||||||
|
q,
|
||||||
|
limit: SEARCH_LIMIT,
|
||||||
|
attributesToRetrieve: [
|
||||||
|
"title",
|
||||||
|
"channel",
|
||||||
|
"uid",
|
||||||
|
"published_date",
|
||||||
|
"views",
|
||||||
|
"duration",
|
||||||
|
"description",
|
||||||
|
],
|
||||||
|
attributesToHighlight: ["title", "channel", "description"],
|
||||||
|
attributesToCrop: ["description"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indexUid: MeiliIndices.POST,
|
||||||
|
q,
|
||||||
|
limit: SEARCH_LIMIT,
|
||||||
|
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"],
|
||||||
|
attributesToHighlight: ["translations.title", "translations.displayable_description"],
|
||||||
|
attributesToCrop: ["translations.displayable_description"],
|
||||||
|
filter: ["hidden = false"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indexUid: MeiliIndices.WEAPON,
|
||||||
|
q,
|
||||||
|
limit: SEARCH_LIMIT,
|
||||||
|
attributesToHighlight: ["translations.description", "translations.names"],
|
||||||
|
attributesToCrop: ["translations.description"],
|
||||||
|
sort: ["slug:asc"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
indexUid: MeiliIndices.WIKI_PAGE,
|
||||||
|
q,
|
||||||
|
limit: SEARCH_LIMIT,
|
||||||
|
attributesToHighlight: [
|
||||||
|
"translations.title",
|
||||||
|
"translations.aliases",
|
||||||
|
"translations.summary",
|
||||||
|
"translations.displayable_description",
|
||||||
|
],
|
||||||
|
attributesToCrop: ["translations.displayable_description"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).results;
|
||||||
|
|
||||||
|
const result: MultiResult = {};
|
||||||
|
|
||||||
|
searchResults.map((searchResult) => {
|
||||||
|
switch (searchResult.indexUid) {
|
||||||
|
case MeiliIndices.LIBRARY_ITEM: {
|
||||||
|
result.libraryItems = filterHitsWithHighlight<MeiliLibraryItem>(
|
||||||
|
searchResult,
|
||||||
|
"descriptions"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.CONTENT: {
|
||||||
|
result.contents = filterHitsWithHighlight<MeiliContent>(searchResult, "translations");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.VIDEOS: {
|
||||||
|
result.videos = filterHitsWithHighlight<MeiliVideo>(searchResult);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.POST: {
|
||||||
|
result.posts = filterHitsWithHighlight<MeiliPost>(searchResult, "translations");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.WEAPON: {
|
||||||
|
result.weapons = filterHitsWithHighlight<MeiliWeapon>(searchResult, "translations");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.WIKI_PAGE: {
|
||||||
|
result.wikiPages = filterHitsWithHighlight<MeiliWikiPage>(searchResult, "translations");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.log("What the fuck?");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
});
|
});
|
||||||
setLibraryItems(searchResult);
|
|
||||||
|
setMultiResult(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchContents = async () => {
|
if (q === "") {
|
||||||
const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, {
|
setMultiResult({});
|
||||||
limit: SEARCH_LIMIT,
|
|
||||||
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
|
|
||||||
attributesToHighlight: ["translations"],
|
|
||||||
attributesToCrop: ["translations.displayable_description"],
|
|
||||||
});
|
|
||||||
searchResult.hits = searchResult.hits.map((item) => {
|
|
||||||
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
|
|
||||||
item._formatted.translations = filterDefined(item._formatted.translations).filter(
|
|
||||||
(translation) => containsHighlight(JSON.stringify(translation))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
setContents(searchResult);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchVideos = async () => {
|
|
||||||
const searchResult = await meiliSearch(MeiliIndices.VIDEOS, query, {
|
|
||||||
limit: SEARCH_LIMIT,
|
|
||||||
attributesToRetrieve: [
|
|
||||||
"title",
|
|
||||||
"channel",
|
|
||||||
"uid",
|
|
||||||
"published_date",
|
|
||||||
"views",
|
|
||||||
"duration",
|
|
||||||
"description",
|
|
||||||
],
|
|
||||||
attributesToHighlight: ["title", "channel", "description"],
|
|
||||||
attributesToCrop: ["description"],
|
|
||||||
});
|
|
||||||
setVideos(searchResult);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPosts = async () => {
|
|
||||||
const searchResult = await meiliSearch(MeiliIndices.POST, query, {
|
|
||||||
limit: SEARCH_LIMIT,
|
|
||||||
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"],
|
|
||||||
attributesToHighlight: ["translations.title", "translations.excerpt", "translations.body"],
|
|
||||||
attributesToCrop: ["translations.body"],
|
|
||||||
filter: ["hidden = false"],
|
|
||||||
});
|
|
||||||
searchResult.hits = searchResult.hits.map((item) => {
|
|
||||||
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
|
|
||||||
item._formatted.translations = filterDefined(item._formatted.translations).filter(
|
|
||||||
(translation) => JSON.stringify(translation).includes("</mark>")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
setPosts(searchResult);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchWikiPages = async () => {
|
|
||||||
const searchResult = await meiliSearch(MeiliIndices.WIKI_PAGE, query, {
|
|
||||||
limit: SEARCH_LIMIT,
|
|
||||||
attributesToHighlight: [
|
|
||||||
"translations.title",
|
|
||||||
"translations.aliases",
|
|
||||||
"translations.summary",
|
|
||||||
"translations.displayable_description",
|
|
||||||
],
|
|
||||||
attributesToCrop: ["translations.displayable_description"],
|
|
||||||
});
|
|
||||||
searchResult.hits = searchResult.hits.map((item) => {
|
|
||||||
if (
|
|
||||||
Object.keys(item._matchesPosition).filter((match) => match.startsWith("translations"))
|
|
||||||
.length > 0
|
|
||||||
) {
|
|
||||||
item._formatted.translations = filterDefined(item._formatted.translations).filter(
|
|
||||||
(translation) => JSON.stringify(translation).includes("</mark>")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
setWikiPages(searchResult);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (query === "") {
|
|
||||||
setWikiPages(undefined);
|
|
||||||
setLibraryItems(undefined);
|
|
||||||
setContents(undefined);
|
|
||||||
setVideos(undefined);
|
|
||||||
setPosts(undefined);
|
|
||||||
} else {
|
} else {
|
||||||
fetchWikiPages();
|
fetchMultiResult();
|
||||||
fetchLibraryItems();
|
|
||||||
fetchContents();
|
|
||||||
fetchVideos();
|
|
||||||
fetchPosts();
|
|
||||||
}
|
}
|
||||||
}, [query]);
|
|
||||||
|
setQuery(q);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
|
@ -176,30 +205,37 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
setSearchOpened(false);
|
setSearchOpened(false);
|
||||||
sendAnalytics("Search", "Close search");
|
sendAnalytics("Search", "Close search");
|
||||||
}}
|
}}
|
||||||
|
onOpen={() => searchInputRef.current?.focus()}
|
||||||
fillViewport>
|
fillViewport>
|
||||||
<h2 className="inline-flex place-items-center gap-2 text-2xl">
|
<h2 className="inline-flex place-items-center gap-2 text-2xl">
|
||||||
<Ico icon="search" isFilled />
|
<Ico icon="search" isFilled />
|
||||||
{format("search")}
|
{format("search")}
|
||||||
</h2>
|
</h2>
|
||||||
<TextInput onChange={setQuery} value={query} placeholder={format("search_title")} />
|
<TextInput
|
||||||
|
ref={searchInputRef}
|
||||||
|
onChange={fetchSearchResults}
|
||||||
|
value={query}
|
||||||
|
placeholder={format("search_placeholder")}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap gap-12 gap-x-16">
|
<div className="flex w-full flex-wrap gap-12 gap-x-16">
|
||||||
{isDefined(libraryItems) && (
|
{isDefined(multiResult.libraryItems) && (
|
||||||
<SearchResultSection
|
<SearchResultSection
|
||||||
title={format("library")}
|
title={format("library")}
|
||||||
icon="auto_stories"
|
icon="auto_stories"
|
||||||
href={`/library?page=1&query=${query}\
|
href={`/library?page=1&query=${query}\
|
||||||
&sort=0&primary=true&secondary=true&subitems=true&status=all`}
|
&sort=0&primary=true&secondary=true&subitems=true&status=all`}
|
||||||
totalHits={libraryItems.estimatedTotalHits}>
|
totalHits={multiResult.libraryItems.estimatedTotalHits}>
|
||||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||||
{libraryItems.hits.map((item) => (
|
{multiResult.libraryItems.hits.map((item) => (
|
||||||
<TranslatedPreviewCard
|
<TranslatedPreviewCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="w-56"
|
className="w-56"
|
||||||
href={`/library/${item.slug}`}
|
href={`/library/${item.slug}`}
|
||||||
|
onClick={() => setSearchOpened(false)}
|
||||||
translations={filterHasAttributes(item._formatted.descriptions, [
|
translations={filterHasAttributes(item._formatted.descriptions, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
] as const).map((translation) => ({
|
]).map((translation) => ({
|
||||||
language: translation.language.data.attributes.code,
|
language: translation.language.data.attributes.code,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
subtitle: item.subtitle,
|
subtitle: item.subtitle,
|
||||||
|
@ -214,11 +250,11 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
keepInfoVisible
|
keepInfoVisible
|
||||||
topChips={
|
topChips={
|
||||||
item.metadata && item.metadata.length > 0 && item.metadata[0]
|
item.metadata && item.metadata.length > 0 && item.metadata[0]
|
||||||
? [prettyItemSubType(item.metadata[0])]
|
? [formatLibraryItemSubType(item.metadata[0])]
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
bottomChips={item.categories?.data.map(
|
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||||
(category) => category.attributes?.short ?? ""
|
(category) => formatCategory(category.attributes.slug)
|
||||||
)}
|
)}
|
||||||
metadata={{
|
metadata={{
|
||||||
releaseDate: item.release_date,
|
releaseDate: item.release_date,
|
||||||
|
@ -231,21 +267,22 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
</SearchResultSection>
|
</SearchResultSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDefined(contents) && (
|
{isDefined(multiResult.contents) && (
|
||||||
<SearchResultSection
|
<SearchResultSection
|
||||||
title={format("contents")}
|
title={format("contents")}
|
||||||
icon="workspaces"
|
icon="workspaces"
|
||||||
href={`/contents/all?page=1&query=${query}&sort=0`}
|
href={`/contents/all?page=1&query=${query}&sort=0`}
|
||||||
totalHits={contents.estimatedTotalHits}>
|
totalHits={multiResult.contents.estimatedTotalHits}>
|
||||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||||
{contents.hits.map((item) => (
|
{multiResult.contents.hits.map((item) => (
|
||||||
<TranslatedPreviewCard
|
<TranslatedPreviewCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="w-56"
|
className="w-56"
|
||||||
href={`/contents/${item.slug}`}
|
href={`/contents/${item.slug}`}
|
||||||
|
onClick={() => setSearchOpened(false)}
|
||||||
translations={filterHasAttributes(item._formatted.translations, [
|
translations={filterHasAttributes(item._formatted.translations, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
] as const).map(({ displayable_description, language, ...otherAttributes }) => ({
|
]).map(({ displayable_description, language, ...otherAttributes }) => ({
|
||||||
...otherAttributes,
|
...otherAttributes,
|
||||||
description: containsHighlight(displayable_description)
|
description: containsHighlight(displayable_description)
|
||||||
? displayable_description
|
? displayable_description
|
||||||
|
@ -258,15 +295,11 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
thumbnailForceAspectRatio
|
thumbnailForceAspectRatio
|
||||||
topChips={
|
topChips={
|
||||||
item.type?.data?.attributes
|
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
|
: undefined
|
||||||
}
|
}
|
||||||
bottomChips={item.categories?.data.map(
|
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||||
(category) => category.attributes?.short ?? ""
|
(category) => formatCategory(category.attributes.slug)
|
||||||
)}
|
)}
|
||||||
keepInfoVisible
|
keepInfoVisible
|
||||||
/>
|
/>
|
||||||
|
@ -275,21 +308,22 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
</SearchResultSection>
|
</SearchResultSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDefined(wikiPages) && (
|
{isDefined(multiResult.wikiPages) && (
|
||||||
<SearchResultSection
|
<SearchResultSection
|
||||||
title={format("wiki")}
|
title={format("wiki")}
|
||||||
icon="travel_explore"
|
icon="travel_explore"
|
||||||
href={`/wiki?page=1&query=${query}`}
|
href={`/wiki?page=1&query=${query}`}
|
||||||
totalHits={wikiPages.estimatedTotalHits}>
|
totalHits={multiResult.wikiPages.estimatedTotalHits}>
|
||||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||||
{wikiPages.hits.map((item) => (
|
{multiResult.wikiPages.hits.map((item) => (
|
||||||
<TranslatedPreviewCard
|
<TranslatedPreviewCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="w-56"
|
className="w-56"
|
||||||
href={`/wiki/${item.slug}`}
|
href={`/wiki/${item.slug}`}
|
||||||
|
onClick={() => setSearchOpened(false)}
|
||||||
translations={filterHasAttributes(item._formatted.translations, [
|
translations={filterHasAttributes(item._formatted.translations, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
] as const).map(
|
]).map(
|
||||||
({
|
({
|
||||||
aliases,
|
aliases,
|
||||||
summary,
|
summary,
|
||||||
|
@ -314,38 +348,37 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
thumbnailRounded
|
thumbnailRounded
|
||||||
thumbnailForceAspectRatio
|
thumbnailForceAspectRatio
|
||||||
keepInfoVisible
|
keepInfoVisible
|
||||||
topChips={filterHasAttributes(item.tags?.data, ["attributes"] as const).map(
|
topChips={filterHasAttributes(item.tags?.data, ["attributes"]).map((tag) =>
|
||||||
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
|
formatWikiTag(tag.attributes.slug)
|
||||||
|
)}
|
||||||
|
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||||
|
(category) => formatCategory(category.attributes.slug)
|
||||||
)}
|
)}
|
||||||
bottomChips={filterHasAttributes(item.categories?.data, [
|
|
||||||
"attributes",
|
|
||||||
] as const).map((category) => category.attributes.short)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SearchResultSection>
|
</SearchResultSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDefined(posts) && (
|
{isDefined(multiResult.posts) && (
|
||||||
<SearchResultSection
|
<SearchResultSection
|
||||||
title={format("news")}
|
title={format("news")}
|
||||||
icon="newspaper"
|
icon="newspaper"
|
||||||
href={`/news?page=1&query=${query}`}
|
href={`/news?page=1&query=${query}`}
|
||||||
totalHits={posts.estimatedTotalHits}>
|
totalHits={multiResult.posts.estimatedTotalHits}>
|
||||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||||
{posts.hits.map((item) => (
|
{multiResult.posts.hits.map((item) => (
|
||||||
<TranslatedPreviewCard
|
<TranslatedPreviewCard
|
||||||
className="w-56"
|
className="w-56"
|
||||||
key={item.id}
|
key={item.id}
|
||||||
href={`/news/${item.slug}`}
|
href={`/news/${item.slug}`}
|
||||||
|
onClick={() => setSearchOpened(false)}
|
||||||
translations={filterHasAttributes(item._formatted.translations, [
|
translations={filterHasAttributes(item._formatted.translations, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
] as const).map(({ excerpt, body, language, ...otherAttributes }) => ({
|
]).map(({ excerpt, displayable_description, language, ...otherAttributes }) => ({
|
||||||
...otherAttributes,
|
...otherAttributes,
|
||||||
description: containsHighlight(excerpt)
|
description: containsHighlight(displayable_description)
|
||||||
? excerpt
|
? displayable_description
|
||||||
: containsHighlight(body)
|
|
||||||
? body
|
|
||||||
: excerpt,
|
: excerpt,
|
||||||
language: language.data.attributes.code,
|
language: language.data.attributes.code,
|
||||||
}))}
|
}))}
|
||||||
|
@ -354,8 +387,8 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
thumbnailAspectRatio="3/2"
|
thumbnailAspectRatio="3/2"
|
||||||
thumbnailForceAspectRatio
|
thumbnailForceAspectRatio
|
||||||
keepInfoVisible
|
keepInfoVisible
|
||||||
bottomChips={item.categories?.data.map(
|
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||||
(category) => category.attributes?.short ?? ""
|
(category) => formatCategory(category.attributes.slug)
|
||||||
)}
|
)}
|
||||||
metadata={{
|
metadata={{
|
||||||
releaseDate: item.date,
|
releaseDate: item.date,
|
||||||
|
@ -368,18 +401,19 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
</SearchResultSection>
|
</SearchResultSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDefined(videos) && (
|
{isDefined(multiResult.videos) && (
|
||||||
<SearchResultSection
|
<SearchResultSection
|
||||||
title={format("videos")}
|
title={format("videos")}
|
||||||
icon="movie"
|
icon="movie"
|
||||||
href={`/archives/videos?page=1&query=${query}&sort=1&gone=`}
|
href={`/archives/videos?page=1&query=${query}&sort=1&gone=`}
|
||||||
totalHits={videos.estimatedTotalHits}>
|
totalHits={multiResult.videos.estimatedTotalHits}>
|
||||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||||
{videos.hits.map((item) => (
|
{multiResult.videos.hits.map((item) => (
|
||||||
<PreviewCard
|
<PreviewCard
|
||||||
className="w-56"
|
className="w-56"
|
||||||
key={item.uid}
|
key={item.uid}
|
||||||
href={`/archives/videos/v/${item.uid}`}
|
href={`/archives/videos/v/${item.uid}`}
|
||||||
|
onClick={() => setSearchOpened(false)}
|
||||||
title={item._formatted.title}
|
title={item._formatted.title}
|
||||||
thumbnail={getVideoThumbnailURL(item.uid)}
|
thumbnail={getVideoThumbnailURL(item.uid)}
|
||||||
thumbnailAspectRatio="16/9"
|
thumbnailAspectRatio="16/9"
|
||||||
|
@ -406,11 +440,56 @@ export const SearchPopup = (): JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
</SearchResultSection>
|
</SearchResultSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isDefined(multiResult.weapons) && (
|
||||||
|
<SearchResultSection
|
||||||
|
title={format("weapon", { count: Infinity })}
|
||||||
|
icon="shield"
|
||||||
|
href={`/wiki/weapons?page=1&query=${query}`}
|
||||||
|
totalHits={multiResult.weapons.estimatedTotalHits}>
|
||||||
|
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||||
|
{multiResult.weapons.hits.map((item) => (
|
||||||
|
<TranslatedPreviewCard
|
||||||
|
key={item.id}
|
||||||
|
className="w-56"
|
||||||
|
href={"/"}
|
||||||
|
translations={filterHasAttributes(item._formatted.translations, [
|
||||||
|
"language.data.attributes.code",
|
||||||
|
]).map(({ description, language, names: [primaryName, ...aliases] }) => ({
|
||||||
|
language: language.data.attributes.code,
|
||||||
|
title: primaryName,
|
||||||
|
subtitle: aliases.join("・"),
|
||||||
|
description: containsHighlight(description) ? description : undefined,
|
||||||
|
}))}
|
||||||
|
fallback={{ title: prettySlug(item.slug) }}
|
||||||
|
thumbnail={item.thumbnail?.data?.attributes}
|
||||||
|
thumbnailAspectRatio="1/1"
|
||||||
|
thumbnailForceAspectRatio
|
||||||
|
thumbnailFitMethod="contain"
|
||||||
|
keepInfoVisible
|
||||||
|
topChips={
|
||||||
|
item.type?.data?.attributes?.slug
|
||||||
|
? [formatWeaponType(item.type.data.attributes.slug)]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
bottomChips={filterHasAttributes(item.categories, ["attributes"]).map(
|
||||||
|
(category) => formatCategory(category.attributes.slug)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SearchResultSection>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭──────────────────────╮
|
||||||
|
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
interface SearchResultSectionProps {
|
interface SearchResultSectionProps {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
icon: MaterialSymbol;
|
icon: MaterialSymbol;
|
||||||
|
@ -427,6 +506,7 @@ const SearchResultSection = ({
|
||||||
children,
|
children,
|
||||||
}: SearchResultSectionProps) => {
|
}: SearchResultSectionProps) => {
|
||||||
const { format } = useFormat();
|
const { format } = useFormat();
|
||||||
|
const setSearchOpened = useAtomSetter(atoms.layout.searchOpened);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDefined(totalHits) && totalHits > 0 && (
|
{isDefined(totalHits) && totalHits > 0 && (
|
||||||
|
@ -434,8 +514,9 @@ const SearchResultSection = ({
|
||||||
<div className="mb-6 grid place-content-start">
|
<div className="mb-6 grid place-content-start">
|
||||||
<UpPressable
|
<UpPressable
|
||||||
className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4"
|
className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4"
|
||||||
href={href}>
|
href={href}
|
||||||
<Ico icon={icon} className="!text-3xl" isFilled />
|
onClick={() => setSearchOpened(false)}>
|
||||||
|
<Ico icon={icon} className="!text-3xl" isFilled={false} />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-headers text-lg">{title}</p>
|
<p className="font-headers text-lg">{title}</p>
|
||||||
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
|
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "components/Inputs/Button";
|
|
||||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||||
import { OrderableList } from "components/Inputs/OrderableList";
|
import { OrderableList } from "components/Inputs/OrderableList";
|
||||||
import { Select } from "components/Inputs/Select";
|
import { Select } from "components/Inputs/Select";
|
||||||
|
@ -8,13 +7,19 @@ import { TextInput } from "components/Inputs/TextInput";
|
||||||
import { Popup } from "components/Containers/Popup";
|
import { Popup } from "components/Containers/Popup";
|
||||||
import { sendAnalytics } from "helpers/analytics";
|
import { sendAnalytics } from "helpers/analytics";
|
||||||
import { cJoin, cIf } from "helpers/className";
|
import { cJoin, cIf } from "helpers/className";
|
||||||
import { prettyLanguage } from "helpers/formatters";
|
|
||||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomGetter, useAtomPair } from "helpers/atoms";
|
import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||||
import { ThemeMode } from "contexts/settings";
|
import { PerfMode, ThemeMode } from "contexts/settings";
|
||||||
import { Ico } from "components/Ico";
|
import { Ico } from "components/Ico";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
import { ToolTip } from "components/ToolTip";
|
||||||
|
import { Switch } from "components/Inputs/Switch";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ╭─────────────╮
|
||||||
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
export const SettingsPopup = (): JSX.Element => {
|
export const SettingsPopup = (): JSX.Element => {
|
||||||
const [preferredLanguages, setPreferredLanguages] = useAtomPair(
|
const [preferredLanguages, setPreferredLanguages] = useAtomPair(
|
||||||
|
@ -26,16 +31,18 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
const [fontSize, setFontSize] = useAtomPair(atoms.settings.fontSize);
|
const [fontSize, setFontSize] = useAtomPair(atoms.settings.fontSize);
|
||||||
const [playerName, setPlayerName] = useAtomPair(atoms.settings.playerName);
|
const [playerName, setPlayerName] = useAtomPair(atoms.settings.playerName);
|
||||||
const [themeMode, setThemeMode] = useAtomPair(atoms.settings.themeMode);
|
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 languages = useAtomGetter(atoms.localData.languages);
|
const { format, formatLanguage } = useFormat();
|
||||||
const { format } = useFormat();
|
|
||||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||||
|
|
||||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const currencyOptions = filterHasAttributes(currencies, ["attributes"] as const).map(
|
const currencyOptions = filterHasAttributes(currencies, ["attributes"]).map(
|
||||||
(currentCurrency) => currentCurrency.attributes.code
|
(currentCurrency) => currentCurrency.attributes.code
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -68,7 +75,7 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
<OrderableList
|
<OrderableList
|
||||||
items={preferredLanguages.map((locale) => ({
|
items={preferredLanguages.map((locale) => ({
|
||||||
code: locale,
|
code: locale,
|
||||||
name: prettyLanguage(locale, languages),
|
name: formatLanguage(locale),
|
||||||
}))}
|
}))}
|
||||||
insertLabels={[
|
insertLabels={[
|
||||||
{
|
{
|
||||||
|
@ -95,7 +102,12 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
cIf(!is1ColumnLayout, "grid-cols-2")
|
cIf(!is1ColumnLayout, "grid-cols-2")
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl">{format("theme")}</h3>
|
<div className="flex place-content-center place-items-center gap-1">
|
||||||
|
<h3 className="text-xl">{format("theme")}</h3>
|
||||||
|
<ToolTip content={format("dark_mode_extension_warning")} placement="top">
|
||||||
|
<Ico icon="info" />
|
||||||
|
</ToolTip>
|
||||||
|
</div>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
buttonsProps={[
|
buttonsProps={[
|
||||||
{
|
{
|
||||||
|
@ -136,7 +148,7 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
const newCurrencyName = currencyOptions[newCurrency];
|
const newCurrencyName = currencyOptions[newCurrency];
|
||||||
if (isDefined(newCurrencyName)) {
|
if (isDefined(newCurrencyName)) {
|
||||||
setCurrency(newCurrencyName);
|
setCurrency(newCurrencyName);
|
||||||
sendAnalytics("Settings", `Change currency (${currencyOptions[newCurrency]})}`);
|
sendAnalytics("Settings", `Change currency (${currencyOptions[newCurrency]})`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-28"
|
className="w-28"
|
||||||
|
@ -188,31 +200,41 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl">{format("font")}</h3>
|
<h3 className="text-xl">{format("font")}</h3>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Button
|
<ButtonGroup
|
||||||
active={!isDyslexic}
|
vertical
|
||||||
onClick={() => {
|
buttonsProps={[
|
||||||
setDyslexic(false);
|
{
|
||||||
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
active: !isDyslexic,
|
||||||
}}
|
onClick: () => {
|
||||||
className="font-zenMaruGothic"
|
setDyslexic(false);
|
||||||
text="Zen Maru Gothic"
|
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
||||||
/>
|
},
|
||||||
<Button
|
className: "font-zenMaruGothic",
|
||||||
active={isDyslexic}
|
text: "Zen Maru Gothic",
|
||||||
onClick={() => {
|
},
|
||||||
setDyslexic(true);
|
{
|
||||||
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
active: isDyslexic,
|
||||||
}}
|
onClick: () => {
|
||||||
className="font-openDyslexic"
|
setDyslexic(true);
|
||||||
text="OpenDyslexic"
|
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
||||||
|
},
|
||||||
|
className: "font-openDyslexic",
|
||||||
|
text: "OpenDyslexic",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl">{format("player_name")}</h3>
|
<div className="flex place-content-center place-items-center gap-1">
|
||||||
|
<h3 className="text-xl">{format("player_name")}</h3>
|
||||||
|
<ToolTip content={format("player_name_tooltip")} placement="top">
|
||||||
|
<Ico icon="info" />
|
||||||
|
</ToolTip>
|
||||||
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="<player>"
|
placeholder="(player)"
|
||||||
className="w-48"
|
className="w-48"
|
||||||
value={playerName}
|
value={playerName}
|
||||||
onChange={(newName) => {
|
onChange={(newName) => {
|
||||||
|
@ -221,6 +243,20 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
|
@ -0,0 +1,301 @@
|
||||||
|
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,18 +1,19 @@
|
||||||
import { Fragment, useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
||||||
import { Chip } from "./Chip";
|
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
||||||
import { HorizontalLine } from "./HorizontalLine";
|
|
||||||
import { Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
|
||||||
import { ReturnButton } from "./PanelComponents/ReturnButton";
|
import { ReturnButton } from "./PanelComponents/ReturnButton";
|
||||||
import { ContentPanel } from "./Containers/ContentPanel";
|
import { ContentPanel } from "./Containers/ContentPanel";
|
||||||
import { SubPanel } from "./Containers/SubPanel";
|
import { SubPanel } from "./Containers/SubPanel";
|
||||||
import { RecorderChip } from "./RecorderChip";
|
|
||||||
import { ThumbnailHeader } from "./ThumbnailHeader";
|
import { ThumbnailHeader } from "./ThumbnailHeader";
|
||||||
import { ToolTip } from "./ToolTip";
|
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
import { PostWithTranslations } from "types/types";
|
import { PostWithTranslations } from "types/types";
|
||||||
import { filterHasAttributes } from "helpers/asserts";
|
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||||
import { prettySlug } from "helpers/formatters";
|
import { prettySlug } from "helpers/formatters";
|
||||||
|
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";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -48,7 +49,9 @@ export const PostPage = ({
|
||||||
displayTitle = true,
|
displayTitle = true,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { format, formatStatusDescription } = useFormat();
|
const { formatCategory } = useFormat();
|
||||||
|
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||||
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||||
|
|
||||||
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||||
items: post.translations,
|
items: post.translations,
|
||||||
|
@ -65,59 +68,31 @@ export const PostPage = ({
|
||||||
const title = selectedTranslation?.title ?? prettySlug(post.slug);
|
const title = selectedTranslation?.title ?? prettySlug(post.slug);
|
||||||
const excerpt = selectedTranslation?.excerpt ?? "";
|
const excerpt = selectedTranslation?.excerpt ?? "";
|
||||||
|
|
||||||
|
const toc = getTocFromMarkdawn(body, title);
|
||||||
|
|
||||||
|
const subPanelElems = [
|
||||||
|
returnHref && returnTitle && !is1ColumnLayout && (
|
||||||
|
<ReturnButton href={returnHref} title={returnTitle} />
|
||||||
|
),
|
||||||
|
|
||||||
|
displayCredits && <Credits status={selectedTranslation?.status} authors={post.authors?.data} />,
|
||||||
|
|
||||||
|
displayToc && isDefined(toc) && (
|
||||||
|
<TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
const subPanel =
|
const subPanel =
|
||||||
returnHref || returnTitle || displayCredits || displayToc ? (
|
subPanelElems.filter(Boolean).length > 0 ? (
|
||||||
<SubPanel>
|
<SubPanel>
|
||||||
{returnHref && returnTitle && (
|
<ElementsSeparator>{subPanelElems}</ElementsSeparator>
|
||||||
<ReturnButton href={returnHref} title={returnTitle} displayOnlyOn={"3ColumnsLayout"} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{displayCredits && (
|
|
||||||
<>
|
|
||||||
<HorizontalLine />
|
|
||||||
|
|
||||||
{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"] as const).map(
|
|
||||||
(author) => (
|
|
||||||
<Fragment key={author.id}>
|
|
||||||
<RecorderChip recorder={author.attributes} />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{displayToc && <TableOfContents text={body} title={title} horizontalLine />}
|
|
||||||
</SubPanel>
|
</SubPanel>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
const contentPanel = (
|
const contentPanel = (
|
||||||
<ContentPanel>
|
<ContentPanel>
|
||||||
{returnHref && returnTitle && (
|
{is1ColumnLayout && returnHref && returnTitle && (
|
||||||
<ReturnButton
|
<ReturnButton href={returnHref} title={returnTitle} className="mb-10" />
|
||||||
href={returnHref}
|
|
||||||
title={returnTitle}
|
|
||||||
displayOnlyOn={"1ColumnLayout"}
|
|
||||||
className="mb-10"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{displayThumbnailHeader ? (
|
{displayThumbnailHeader ? (
|
||||||
|
@ -126,13 +101,17 @@ export const PostPage = ({
|
||||||
thumbnail={thumbnail}
|
thumbnail={thumbnail}
|
||||||
title={title}
|
title={title}
|
||||||
description={excerpt}
|
description={excerpt}
|
||||||
categories={post.categories}
|
categories={filterHasAttributes(post.categories?.data, ["attributes"]).map((category) =>
|
||||||
|
formatCategory(category.attributes.slug)
|
||||||
|
)}
|
||||||
|
releaseDate={post.date}
|
||||||
languageSwitcher={
|
languageSwitcher={
|
||||||
languageSwitcherProps.locales.size > 1 ? (
|
languageSwitcherProps.locales.size > 1 ? (
|
||||||
<LanguageSwitcher {...languageSwitcherProps} />
|
<LanguageSwitcher {...languageSwitcherProps} />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<HorizontalLine />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -148,12 +127,7 @@ export const PostPage = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{prependBody}
|
{prependBody}
|
||||||
{body && (
|
{body && <Markdawn text={body} />}
|
||||||
<>
|
|
||||||
{displayThumbnailHeader && <HorizontalLine />}
|
|
||||||
<Markdawn text={body} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{appendBody}
|
{appendBody}
|
||||||
</ContentPanel>
|
</ContentPanel>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback } from "react";
|
import { MouseEventHandler, useCallback } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { Markdown } from "./Markdown/Markdown";
|
import { Markdown } from "./Markdown/Markdown";
|
||||||
import { Chip } from "components/Chip";
|
import { Chip } from "components/Chip";
|
||||||
import { Ico } from "components/Ico";
|
import { Ico } from "components/Ico";
|
||||||
|
@ -7,13 +6,15 @@ import { Img } from "components/Img";
|
||||||
import { UpPressable } from "components/Containers/UpPressable";
|
import { UpPressable } from "components/Containers/UpPressable";
|
||||||
import { DatePickerFragment, PricePickerFragment, UploadImageFragment } from "graphql/generated";
|
import { DatePickerFragment, PricePickerFragment, UploadImageFragment } from "graphql/generated";
|
||||||
import { cIf, cJoin } from "helpers/className";
|
import { cIf, cJoin } from "helpers/className";
|
||||||
import { prettyDate, prettyDuration, prettyPrice, prettyShortenNumber } from "helpers/formatters";
|
import { prettyDuration, prettyShortenNumber } from "helpers/formatters";
|
||||||
import { ImageQuality } from "helpers/img";
|
import { ImageQuality } from "helpers/img";
|
||||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
import { TranslatedProps } from "types/TranslatedProps";
|
import { TranslatedProps } from "types/TranslatedProps";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
import { isDefined } from "helpers/asserts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -24,6 +25,7 @@ interface Props {
|
||||||
thumbnail?: UploadImageFragment | string | null | undefined;
|
thumbnail?: UploadImageFragment | string | null | undefined;
|
||||||
thumbnailAspectRatio?: string;
|
thumbnailAspectRatio?: string;
|
||||||
thumbnailForceAspectRatio?: boolean;
|
thumbnailForceAspectRatio?: boolean;
|
||||||
|
thumbnailFitMethod?: "contain" | "cover";
|
||||||
thumbnailRounded?: boolean;
|
thumbnailRounded?: boolean;
|
||||||
href: string;
|
href: string;
|
||||||
pre_title?: string | null | undefined;
|
pre_title?: string | null | undefined;
|
||||||
|
@ -50,6 +52,7 @@ interface Props {
|
||||||
| { __typename: "anotherHoverlayName" };
|
| { __typename: "anotherHoverlayName" };
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
@ -59,6 +62,7 @@ export const PreviewCard = ({
|
||||||
thumbnail,
|
thumbnail,
|
||||||
thumbnailAspectRatio = "4/3",
|
thumbnailAspectRatio = "4/3",
|
||||||
thumbnailForceAspectRatio = false,
|
thumbnailForceAspectRatio = false,
|
||||||
|
thumbnailFitMethod = "cover",
|
||||||
thumbnailRounded = true,
|
thumbnailRounded = true,
|
||||||
pre_title,
|
pre_title,
|
||||||
title,
|
title,
|
||||||
|
@ -72,26 +76,27 @@ export const PreviewCard = ({
|
||||||
infoAppend,
|
infoAppend,
|
||||||
className,
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
onClick,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const currency = useAtomGetter(atoms.settings.currency);
|
const { formatPrice, formatDate } = useFormat();
|
||||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||||
|
const preferredCurrency = useAtomGetter(atoms.settings.currency);
|
||||||
const isHoverable = useDeviceSupportsHover();
|
const isHoverable = useDeviceSupportsHover();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const metadataJSX = (
|
const metadataJSX = (
|
||||||
<>
|
<>
|
||||||
{metadata && (metadata.releaseDate || metadata.price) && (
|
{metadata && (isDefined(metadata.releaseDate) || isDefined(metadata.price)) && (
|
||||||
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
||||||
{metadata.releaseDate && (
|
{metadata.releaseDate && (
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" />
|
<Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" />
|
||||||
{prettyDate(metadata.releaseDate, router.locale)}
|
{formatDate(metadata.releaseDate)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{metadata.price && (
|
{metadata.price && (
|
||||||
<p className="justify-self-end text-sm">
|
<p className="justify-self-end text-sm">
|
||||||
<Ico icon="shopping_cart" className="mr-1 translate-y-[.15em] !text-base" />
|
<Ico icon="shopping_cart" className="mr-1 translate-y-[.15em] !text-base" />
|
||||||
{prettyPrice(metadata.price, currencies, currency)}
|
{formatPrice(metadata.price, preferredCurrency)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{metadata.views && (
|
{metadata.views && (
|
||||||
|
@ -113,8 +118,9 @@ export const PreviewCard = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UpPressable
|
<UpPressable
|
||||||
className={cJoin("grid items-end text-left", className)}
|
className={cJoin("relative grid items-end text-left", className)}
|
||||||
href={href}
|
href={href}
|
||||||
|
onClick={onClick}
|
||||||
noBackground
|
noBackground
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
<div className={cJoin("group", cIf(disabled, "pointer-events-none touch-none select-none"))}>
|
<div className={cJoin("group", cIf(disabled, "pointer-events-none touch-none select-none"))}>
|
||||||
|
@ -130,7 +136,12 @@ export const PreviewCard = ({
|
||||||
thumbnailRounded,
|
thumbnailRounded,
|
||||||
cIf(keepInfoVisible, "rounded-t-md", "rounded-md notHoverable:rounded-b-none")
|
cIf(keepInfoVisible, "rounded-t-md", "rounded-md notHoverable:rounded-b-none")
|
||||||
),
|
),
|
||||||
cIf(thumbnailForceAspectRatio, "h-full w-full object-cover")
|
cIf(thumbnailForceAspectRatio, "h-full w-full"),
|
||||||
|
cIf(
|
||||||
|
thumbnailForceAspectRatio && thumbnailFitMethod === "contain",
|
||||||
|
"object-contain",
|
||||||
|
"object-cover"
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
src={thumbnail}
|
src={thumbnail}
|
||||||
quality={ImageQuality.Medium}
|
quality={ImageQuality.Medium}
|
||||||
|
@ -139,17 +150,15 @@ export const PreviewCard = ({
|
||||||
{hoverlay && hoverlay.__typename === "Video" && (
|
{hoverlay && hoverlay.__typename === "Video" && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 grid place-content-center bg-shade bg-opacity-0
|
className="absolute inset-0 grid place-content-center rounded-t-md
|
||||||
text-light transition-colors group-hover:bg-opacity-50">
|
bg-shade/0 text-light transition-colors group-hover:bg-shade/50">
|
||||||
<Ico
|
<Ico
|
||||||
icon="play_circle"
|
icon="play_circle"
|
||||||
className="!text-6xl text-light opacity-0 drop-shadow-lg transition-opacity
|
className="!text-6xl text-light opacity-0 drop-shadow-lg transition-opacity
|
||||||
shadow-shade group-hover:opacity-100 dark:text-black"
|
shadow-shade group-hover:opacity-100 dark:text-black"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="absolute bottom-2 right-2 rounded-full bg-black/60 px-2 text-light">
|
||||||
className="absolute right-2 bottom-2 rounded-full bg-black/60 px-2
|
|
||||||
text-light">
|
|
||||||
{prettyDuration(hoverlay.duration)}
|
{prettyDuration(hoverlay.duration)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -169,11 +178,11 @@ export const PreviewCard = ({
|
||||||
"z-20 grid gap-2 p-4 transition-opacity linearbg-obi",
|
"z-20 grid gap-2 p-4 transition-opacity linearbg-obi",
|
||||||
cIf(
|
cIf(
|
||||||
!keepInfoVisible && isHoverable,
|
!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%]
|
[border-radius:10%_10%_10%_10%_/_1%_1%_3%_3%]
|
||||||
group-hover:opacity-100 hoverable:absolute hoverable:drop-shadow-lg
|
group-hover:opacity-100 hoverable:absolute hoverable:shadow-lg
|
||||||
notHoverable:rounded-b-md notHoverable:opacity-100`,
|
notHoverable:rounded-b-md notHoverable:opacity-100`,
|
||||||
"[border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]"
|
cIf(!isPerfModeEnabled, "[border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]")
|
||||||
)
|
)
|
||||||
)}>
|
)}>
|
||||||
{metadata?.position === "Top" && metadataJSX}
|
{metadata?.position === "Top" && metadataJSX}
|
||||||
|
@ -196,7 +205,7 @@ export const PreviewCard = ({
|
||||||
)}
|
)}
|
||||||
{subtitle && <Markdown text={subtitle} className="leading-none break-words" />}
|
{subtitle && <Markdown text={subtitle} className="leading-none break-words" />}
|
||||||
</div>
|
</div>
|
||||||
{description && <Markdown text={description} className="break-words" />}
|
{description && <Markdown text={description} className="overflow-hidden break-words" />}
|
||||||
{bottomChips && bottomChips.length > 0 && (
|
{bottomChips && bottomChips.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
import { useCallback } from "react";
|
|
||||||
import { Chip } from "./Chip";
|
|
||||||
import { Img } from "./Img";
|
|
||||||
import { UpPressable } from "./Containers/UpPressable";
|
|
||||||
import { UploadImageFragment } from "graphql/generated";
|
|
||||||
import { ImageQuality } from "helpers/img";
|
|
||||||
import { TranslatedProps } from "types/TranslatedProps";
|
|
||||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
|
||||||
import { cIf, cJoin } from "helpers/className";
|
|
||||||
import { isDefined } from "helpers/asserts";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ╭─────────────╮
|
|
||||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
thumbnail?: UploadImageFragment | string | null | undefined;
|
|
||||||
href: string;
|
|
||||||
pre_title?: string | null | undefined;
|
|
||||||
title: string | null | undefined;
|
|
||||||
subtitle?: string | null | undefined;
|
|
||||||
topChips?: string[];
|
|
||||||
bottomChips?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
||||||
|
|
||||||
export const PreviewLine = ({
|
|
||||||
href,
|
|
||||||
thumbnail,
|
|
||||||
pre_title,
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
topChips,
|
|
||||||
disabled,
|
|
||||||
bottomChips,
|
|
||||||
}: Props): JSX.Element => (
|
|
||||||
<UpPressable href={href} disabled={disabled}>
|
|
||||||
<div
|
|
||||||
className={cJoin(
|
|
||||||
"grid w-full grid-flow-col place-items-center gap-4",
|
|
||||||
cIf(disabled, "pointer-events-none touch-none select-none")
|
|
||||||
)}>
|
|
||||||
{thumbnail && (
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<Img className="h-full object-cover" src={thumbnail} quality={ImageQuality.Medium} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={cJoin("grid gap-2 py-4", cIf(isDefined(thumbnail), "pr-3", "px-6"))}>
|
|
||||||
{topChips && topChips.length > 0 && (
|
|
||||||
<div
|
|
||||||
className="grid grid-flow-col place-content-start gap-1 overflow-scroll
|
|
||||||
scrollbar-none">
|
|
||||||
{topChips.map((text, index) => (
|
|
||||||
<Chip key={index} text={text} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="my-1 flex flex-col">
|
|
||||||
{pre_title && <p className="mb-1 leading-none">{pre_title}</p>}
|
|
||||||
{title && <p className="font-headers text-lg font-bold leading-none">{title}</p>}
|
|
||||||
{subtitle && <p className="leading-none">{subtitle}</p>}
|
|
||||||
</div>
|
|
||||||
{bottomChips && bottomChips.length > 0 && (
|
|
||||||
<div
|
|
||||||
className="grid grid-flow-col place-content-start gap-1 overflow-scroll
|
|
||||||
scrollbar-none">
|
|
||||||
{bottomChips.map((text, index) => (
|
|
||||||
<Chip key={index} className="text-sm" text={text} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UpPressable>
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ╭──────────────────────╮
|
|
||||||
* ───────────────────────────────────╯ TRANSLATED VARIANT ╰──────────────────────────────────────
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const TranslatedPreviewLine = ({
|
|
||||||
translations,
|
|
||||||
fallback,
|
|
||||||
...otherProps
|
|
||||||
}: TranslatedProps<Props, "pre_title" | "subtitle" | "title">): JSX.Element => {
|
|
||||||
const [selectedTranslation] = useSmartLanguage({
|
|
||||||
items: translations,
|
|
||||||
languageExtractor: useCallback((item: { language: string }): string => item.language, []),
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<PreviewLine
|
|
||||||
pre_title={selectedTranslation?.pre_title ?? fallback.pre_title}
|
|
||||||
title={selectedTranslation?.title ?? fallback.title}
|
|
||||||
subtitle={selectedTranslation?.subtitle ?? fallback.subtitle}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -3,10 +3,12 @@ import { Img } from "./Img";
|
||||||
import { Markdawn } from "./Markdown/Markdawn";
|
import { Markdawn } from "./Markdown/Markdawn";
|
||||||
import { ToolTip } from "./ToolTip";
|
import { ToolTip } from "./ToolTip";
|
||||||
import { Chip } from "components/Chip";
|
import { Chip } from "components/Chip";
|
||||||
import { RecorderChipFragment } from "graphql/generated";
|
|
||||||
import { ImageQuality } from "helpers/img";
|
import { ImageQuality } from "helpers/img";
|
||||||
import { filterHasAttributes } from "helpers/asserts";
|
import { filterHasAttributes, isUndefined } from "helpers/asserts";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
|
import { atoms } from "contexts/atoms";
|
||||||
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭─────────────╮
|
* ╭─────────────╮
|
||||||
|
@ -14,14 +16,22 @@ import { useFormat } from "hooks/useFormat";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
username: string;
|
||||||
recorder: RecorderChipFragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export const RecorderChip = ({ recorder }: Props): JSX.Element => {
|
export const RecorderChip = ({ username }: Props): JSX.Element => {
|
||||||
const { format } = useFormat();
|
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 (
|
return (
|
||||||
<ToolTip
|
<ToolTip
|
||||||
|
@ -40,13 +50,11 @@ export const RecorderChip = ({ recorder }: Props): JSX.Element => {
|
||||||
{recorder.languages?.data && recorder.languages.data.length > 0 && (
|
{recorder.languages?.data && recorder.languages.data.length > 0 && (
|
||||||
<div className="flex flex-row flex-wrap gap-1">
|
<div className="flex flex-row flex-wrap gap-1">
|
||||||
<p>{format("language", { count: recorder.languages.data.length })}:</p>
|
<p>{format("language", { count: recorder.languages.data.length })}:</p>
|
||||||
{filterHasAttributes(recorder.languages.data, ["attributes"] as const).map(
|
{filterHasAttributes(recorder.languages.data, ["attributes"]).map((language) => (
|
||||||
(language) => (
|
<Fragment key={language.__typename}>
|
||||||
<Fragment key={language.__typename}>
|
<Chip text={language.attributes.code.toUpperCase()} />
|
||||||
<Chip text={language.attributes.code.toUpperCase()} />
|
</Fragment>
|
||||||
</Fragment>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{recorder.pronouns && (
|
{recorder.pronouns && (
|
||||||
|
@ -57,7 +65,7 @@ export const RecorderChip = ({ recorder }: Props): JSX.Element => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />}
|
{selectedBioTranslation?.bio && <Markdawn text={selectedBioTranslation.bio} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="top">
|
placement="top">
|
||||||
|
|
|
@ -1,233 +0,0 @@
|
||||||
import { Fragment, useCallback, useEffect, useState } from "react";
|
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
|
||||||
import naturalCompare from "string-natural-compare";
|
|
||||||
import { Chip } from "./Chip";
|
|
||||||
import { PageSelector } from "./Inputs/PageSelector";
|
|
||||||
import { Ico } from "./Ico";
|
|
||||||
import { cJoin } from "helpers/className";
|
|
||||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
|
||||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
|
||||||
import { Ids } from "types/ids";
|
|
||||||
import { atoms } from "contexts/atoms";
|
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
|
||||||
import { useFormat } from "hooks/useFormat";
|
|
||||||
|
|
||||||
interface Group<T> {
|
|
||||||
name: string;
|
|
||||||
items: T[];
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultGroupSortingFunction = <T,>(a: Group<T>, b: Group<T>) =>
|
|
||||||
naturalCompare(a.name, b.name);
|
|
||||||
const defaultGroupCountingFunction = () => 1;
|
|
||||||
const defaultFilteringFunction = () => true;
|
|
||||||
const defaultSortingFunction = () => 0;
|
|
||||||
const defaultGroupingFunction = () => [""];
|
|
||||||
|
|
||||||
interface Props<T> {
|
|
||||||
// Items
|
|
||||||
items: T[];
|
|
||||||
getItemId: (item: T) => string;
|
|
||||||
renderItem: (props: { item: T }) => JSX.Element;
|
|
||||||
renderWhenEmpty?: () => JSX.Element;
|
|
||||||
// Pagination
|
|
||||||
paginationItemPerPage?: number;
|
|
||||||
paginationSelectorTop?: boolean;
|
|
||||||
paginationSelectorBottom?: boolean;
|
|
||||||
paginationScroolTop?: boolean;
|
|
||||||
// Searching
|
|
||||||
searchingTerm?: string;
|
|
||||||
searchingBy?: (item: T) => string;
|
|
||||||
searchingCaseInsensitive?: boolean;
|
|
||||||
// Grouping
|
|
||||||
groupingFunction?: (item: T) => string[];
|
|
||||||
groupSortingFunction?: (a: Group<T>, b: Group<T>) => number;
|
|
||||||
groupCountingFunction?: (item: T) => number;
|
|
||||||
// Filtering
|
|
||||||
filteringFunction?: (item: T) => boolean;
|
|
||||||
// Sorting
|
|
||||||
sortingFunction?: (a: T, b: T) => number;
|
|
||||||
// Other
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SmartList = <T,>({
|
|
||||||
items,
|
|
||||||
getItemId,
|
|
||||||
renderItem: RenderItem,
|
|
||||||
renderWhenEmpty: RenderWhenEmpty,
|
|
||||||
paginationItemPerPage = Infinity,
|
|
||||||
paginationSelectorTop = true,
|
|
||||||
paginationSelectorBottom = true,
|
|
||||||
paginationScroolTop = true,
|
|
||||||
searchingTerm,
|
|
||||||
searchingBy,
|
|
||||||
searchingCaseInsensitive = true,
|
|
||||||
groupingFunction = defaultGroupingFunction,
|
|
||||||
groupSortingFunction = defaultGroupSortingFunction,
|
|
||||||
groupCountingFunction = defaultGroupCountingFunction,
|
|
||||||
filteringFunction = defaultFilteringFunction,
|
|
||||||
sortingFunction = defaultSortingFunction,
|
|
||||||
className,
|
|
||||||
}: Props<T>): JSX.Element => {
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const { format } = useFormat();
|
|
||||||
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
|
|
||||||
useEffect(() => setPage(1), [searchingTerm, groupingFunction, groupSortingFunction]);
|
|
||||||
|
|
||||||
const searchFilter = useCallback(() => {
|
|
||||||
if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {
|
|
||||||
if (searchingCaseInsensitive) {
|
|
||||||
return items.filter((item) =>
|
|
||||||
searchingBy(item).toLowerCase().includes(searchingTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return items.filter((item) => searchingBy(item).includes(searchingTerm));
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}, [items, searchingBy, searchingCaseInsensitive, searchingTerm]);
|
|
||||||
|
|
||||||
const filteredItems = searchFilter().filter(filteringFunction);
|
|
||||||
|
|
||||||
const sortedItem = filteredItems.sort(sortingFunction);
|
|
||||||
|
|
||||||
const groups = (() => {
|
|
||||||
const memo: Group<T>[] = [];
|
|
||||||
|
|
||||||
sortedItem.forEach((item) => {
|
|
||||||
groupingFunction(item).forEach((groupName) => {
|
|
||||||
const group = memo.find((elem) => elem.name === groupName);
|
|
||||||
if (isDefined(group)) {
|
|
||||||
group.items.push(item);
|
|
||||||
group.totalCount += groupCountingFunction(item);
|
|
||||||
} else {
|
|
||||||
memo.push({
|
|
||||||
name: groupName,
|
|
||||||
items: [item],
|
|
||||||
totalCount: groupCountingFunction(item),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return memo.sort(groupSortingFunction);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const pages = (() => {
|
|
||||||
const memo: Group<T>[][] = [];
|
|
||||||
let currentPage: Group<T>[] = [];
|
|
||||||
let remainingSlots = paginationItemPerPage;
|
|
||||||
let loopSafeguard = 1000;
|
|
||||||
|
|
||||||
const newPage = () => {
|
|
||||||
memo.push(currentPage);
|
|
||||||
currentPage = [];
|
|
||||||
remainingSlots = paginationItemPerPage;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
let remainingItems = group.items.length;
|
|
||||||
while (remainingItems > 0 && loopSafeguard >= 0) {
|
|
||||||
loopSafeguard--;
|
|
||||||
const currentIndex = group.items.length - remainingItems;
|
|
||||||
|
|
||||||
if (
|
|
||||||
remainingSlots <= 0 ||
|
|
||||||
(currentIndex === 0 &&
|
|
||||||
remainingItems > remainingSlots &&
|
|
||||||
remainingItems <= paginationItemPerPage)
|
|
||||||
) {
|
|
||||||
newPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
const slicedGroup: Group<T> = {
|
|
||||||
name: group.name,
|
|
||||||
items: group.items.slice(currentIndex, currentIndex + remainingSlots),
|
|
||||||
totalCount: group.totalCount,
|
|
||||||
};
|
|
||||||
|
|
||||||
remainingItems -= slicedGroup.items.length;
|
|
||||||
remainingSlots -= slicedGroup.items.length;
|
|
||||||
currentPage.push(slicedGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPage.length > 0) {
|
|
||||||
newPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return memo;
|
|
||||||
})();
|
|
||||||
|
|
||||||
useHotkeys("left", () => setPage((current) => current - 1), { enabled: page > 1 });
|
|
||||||
useHotkeys("right", () => setPage((current) => current + 1), {
|
|
||||||
enabled: page < pages.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pages.length > 1 && paginationSelectorTop && (
|
|
||||||
<PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-8">
|
|
||||||
{(pages[page - 1]?.length ?? 0) > 0 ? (
|
|
||||||
pages[page - 1]?.map(
|
|
||||||
(group) =>
|
|
||||||
group.items.length > 0 && (
|
|
||||||
<Fragment key={group.name}>
|
|
||||||
{group.name.length > 0 && (
|
|
||||||
<h2
|
|
||||||
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
|
|
||||||
first-of-type:pt-0">
|
|
||||||
{group.name}
|
|
||||||
<Chip text={format("x_results", { x: group.totalCount })} />
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cJoin(
|
|
||||||
`grid items-start gap-8 border-b-2 border-dotted pb-12
|
|
||||||
last-of-type:border-0`,
|
|
||||||
className
|
|
||||||
)}>
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<RenderItem item={item} key={getItemId(item)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : isDefined(RenderWhenEmpty) ? (
|
|
||||||
<RenderWhenEmpty />
|
|
||||||
) : (
|
|
||||||
<DefaultRenderWhenEmpty />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pages.length > 1 && paginationSelectorBottom && (
|
|
||||||
<PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ╭──────────────────────╮
|
|
||||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DefaultRenderWhenEmpty = () => {
|
|
||||||
const { format } = useFormat();
|
|
||||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
|
||||||
return (
|
|
||||||
<div className="grid h-full place-content-center">
|
|
||||||
<div
|
|
||||||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
|
|
||||||
border-dark p-8 text-dark opacity-40">
|
|
||||||
{is3ColumnsLayout && <Ico icon="chevron_left" className="!text-[300%]" />}
|
|
||||||
<p className="max-w-xs text-2xl">{format("no_results_message")}</p>
|
|
||||||
{!is3ColumnsLayout && <Ico icon="chevron_right" className="!text-[300%]" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -2,10 +2,9 @@ import { Chip } from "components/Chip";
|
||||||
import { Img } from "components/Img";
|
import { Img } from "components/Img";
|
||||||
import { InsetBox } from "components/Containers/InsetBox";
|
import { InsetBox } from "components/Containers/InsetBox";
|
||||||
import { Markdawn } from "components/Markdown/Markdawn";
|
import { Markdawn } from "components/Markdown/Markdawn";
|
||||||
import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
|
import { DatePickerFragment, UploadImageFragment } from "graphql/generated";
|
||||||
import { prettyInlineTitle, prettySlug, slugify } from "helpers/formatters";
|
import { prettyInlineTitle, slugify } from "helpers/formatters";
|
||||||
import { ImageQuality } from "helpers/img";
|
import { ImageQuality } from "helpers/img";
|
||||||
import { filterHasAttributes } from "helpers/asserts";
|
|
||||||
import { useAtomGetter } from "helpers/atoms";
|
import { useAtomGetter } from "helpers/atoms";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
|
@ -20,14 +19,11 @@ interface Props {
|
||||||
title: string | null | undefined;
|
title: string | null | undefined;
|
||||||
subtitle?: string | null | undefined;
|
subtitle?: string | null | undefined;
|
||||||
description?: string | null | undefined;
|
description?: string | null | undefined;
|
||||||
type?: NonNullable<
|
type?: string;
|
||||||
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
|
categories?: string[];
|
||||||
>["type"];
|
releaseDate?: DatePickerFragment;
|
||||||
categories?: NonNullable<
|
|
||||||
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
|
|
||||||
>["categories"];
|
|
||||||
thumbnail?: UploadImageFragment | null | undefined;
|
thumbnail?: UploadImageFragment | null | undefined;
|
||||||
|
className?: string;
|
||||||
languageSwitcher?: JSX.Element;
|
languageSwitcher?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +38,15 @@ export const ThumbnailHeader = ({
|
||||||
categories,
|
categories,
|
||||||
description,
|
description,
|
||||||
languageSwitcher,
|
languageSwitcher,
|
||||||
|
releaseDate,
|
||||||
|
className,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { format } = useFormat();
|
const { format, formatDate } = useFormat();
|
||||||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={className}>
|
||||||
<div className="mb-12 grid place-items-center gap-12">
|
<div className={"mb-12 grid place-items-center gap-12"}>
|
||||||
<div className="drop-shadow-lg shadow-shade">
|
<div className="drop-shadow-lg shadow-shade">
|
||||||
{thumbnail ? (
|
{thumbnail ? (
|
||||||
<Img
|
<Img
|
||||||
|
@ -71,34 +69,37 @@ export const ThumbnailHeader = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flew-wrap flex flex-row place-content-center gap-8">
|
<div className="flew-wrap flex flex-row place-content-center gap-8">
|
||||||
{type?.data?.attributes && (
|
{type && (
|
||||||
<div className="flex flex-col place-items-center gap-2">
|
<div className="flex flex-col place-items-center gap-2">
|
||||||
<h3 className="text-xl">{format("type", { count: 1 })}</h3>
|
<h3 className="text-xl">{format("type", { count: 1 })}</h3>
|
||||||
<div className="flex flex-row flex-wrap">
|
<div className="flex flex-row flex-wrap">
|
||||||
<Chip
|
<Chip text={type} />
|
||||||
text={
|
|
||||||
type.data.attributes.titles?.[0]?.title ?? prettySlug(type.data.attributes.slug)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{categories && categories.data.length > 0 && (
|
{releaseDate && (
|
||||||
<div className="flex flex-col place-items-center gap-2">
|
<div className="flex flex-col place-items-center gap-2">
|
||||||
<h3 className="text-xl">{format("category", { count: categories.data.length })}</h3>
|
<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>
|
||||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||||
{filterHasAttributes(categories.data, ["attributes", "id"] as const).map(
|
{categories.map((category) => (
|
||||||
(category) => (
|
<Chip key={category} text={category} />
|
||||||
<Chip key={category.id} text={category.attributes.name} />
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{languageSwitcher}
|
{languageSwitcher}
|
||||||
</div>
|
</div>
|
||||||
{description && <InsetBox className="mt-8">{<Markdawn text={description} />}</InsetBox>}
|
{description && <InsetBox className="mt-8">{<Markdawn text={description} />}</InsetBox>}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// eslint-disable-next-line import/named
|
import Tippy from "@tippyjs/react";
|
||||||
import Tippy, { TippyProps } from "@tippyjs/react";
|
import type { TippyProps } from "@tippyjs/react";
|
||||||
import { cJoin } from "helpers/className";
|
import { cJoin } from "helpers/className";
|
||||||
import "tippy.js/animations/scale-subtle.css";
|
import "tippy.js/animations/scale-subtle.css";
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,19 @@
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffectOnce } from "usehooks-ts";
|
import { useEffectOnce } from "usehooks-ts";
|
||||||
import { atom } from "jotai";
|
|
||||||
import { UploadImageFragment } from "graphql/generated";
|
import { UploadImageFragment } from "graphql/generated";
|
||||||
import { LightBox } from "components/LightBox";
|
import { LightBox } from "components/LightBox";
|
||||||
import { filterDefined } from "helpers/asserts";
|
import { filterDefined } from "helpers/asserts";
|
||||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
import { useAtomSetter } from "helpers/atoms";
|
||||||
|
import { internalAtoms } from "contexts/atoms";
|
||||||
const lightBoxAtom = atomPairing(
|
|
||||||
atom<{
|
|
||||||
showLightBox: (
|
|
||||||
images: (UploadImageFragment | string | null | undefined)[],
|
|
||||||
index?: number
|
|
||||||
) => void;
|
|
||||||
}>({ showLightBox: () => null })
|
|
||||||
);
|
|
||||||
|
|
||||||
export const lightBox = lightBoxAtom[0];
|
|
||||||
|
|
||||||
export const LightBoxProvider = (): JSX.Element => {
|
export const LightBoxProvider = (): JSX.Element => {
|
||||||
|
const router = useRouter();
|
||||||
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
|
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
|
||||||
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
|
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
|
||||||
const [lightBoxIndex, setLightBoxIndex] = useState(0);
|
const [lightBoxIndex, setLightBoxIndex] = useState(0);
|
||||||
|
|
||||||
const setShowLightBox = useAtomSetter(lightBoxAtom);
|
const setShowLightBox = useAtomSetter(internalAtoms.lightBox);
|
||||||
|
|
||||||
useEffectOnce(() =>
|
useEffectOnce(() =>
|
||||||
setShowLightBox({
|
setShowLightBox({
|
||||||
|
@ -40,6 +31,8 @@ export const LightBoxProvider = (): JSX.Element => {
|
||||||
setTimeout(() => setLightBoxImages([]), 100);
|
setTimeout(() => setLightBoxImages([]), 100);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => router.events.on("routeChangeStart", closeLightBox));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LightBox
|
<LightBox
|
||||||
isVisible={isLightBoxVisible}
|
isVisible={isLightBoxVisible}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useScrollIntoView } from "hooks/useScrollIntoView";
|
|
||||||
import { useAtomSetter } from "helpers/atoms";
|
|
||||||
import { atoms } from "contexts/atoms";
|
|
||||||
|
|
||||||
export const useAppLayout = (): void => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const setSearchOpened = useAtomSetter(atoms.layout.searchOpened);
|
|
||||||
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
|
|
||||||
const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened);
|
|
||||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.events.on("routeChangeStart", () => {
|
|
||||||
console.log("[Router Events] on routeChangeStart");
|
|
||||||
setSearchOpened(false);
|
|
||||||
setSettingsOpened(false);
|
|
||||||
setMainPanelOpened(false);
|
|
||||||
setSubPanelOpened(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.events.on("hashChangeStart", () => {
|
|
||||||
console.log("[Router Events] on hashChangeStart");
|
|
||||||
setSubPanelOpened(false);
|
|
||||||
});
|
|
||||||
}, [router, setSettingsOpened, setMainPanelOpened, setSubPanelOpened, setSearchOpened]);
|
|
||||||
|
|
||||||
useScrollIntoView();
|
|
||||||
};
|
|
|
@ -1,15 +1,55 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { atomWithStorage } from "jotai/utils";
|
import { atomWithStorage } from "jotai/utils";
|
||||||
import { localData } from "contexts/localData";
|
|
||||||
import { containerQueries } from "contexts/containerQueries";
|
import { containerQueries } from "contexts/containerQueries";
|
||||||
|
import { userAgent } from "contexts/userAgent";
|
||||||
import { atomPairing } from "helpers/atoms";
|
import { atomPairing } from "helpers/atoms";
|
||||||
import { settings } from "contexts/settings";
|
import { settings } from "contexts/settings";
|
||||||
import { lightBox } from "contexts/LightBoxProvider";
|
import { UploadImageFragment } from "graphql/generated";
|
||||||
|
import { Languages, Currencies, Langui, Recorders, TypesTranslations } from "helpers/localData";
|
||||||
|
|
||||||
/*
|
/* [ LOCAL DATA ATOMS ] */
|
||||||
* I'm getting a weird error if I put those atoms in appLayout.ts
|
|
||||||
* So I'm putting the atoms here. Sucks, I know.
|
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 ] */
|
||||||
|
|
||||||
|
const lightBoxAtom = atomPairing(
|
||||||
|
atom<{
|
||||||
|
showLightBox: (
|
||||||
|
images: (UploadImageFragment | string | null | undefined)[],
|
||||||
|
index?: number
|
||||||
|
) => void;
|
||||||
|
}>({ showLightBox: () => null })
|
||||||
|
);
|
||||||
|
|
||||||
|
const lightBox = lightBoxAtom[0];
|
||||||
|
|
||||||
/* [ APPLAYOUT ATOMS ] */
|
/* [ APPLAYOUT ATOMS ] */
|
||||||
|
|
||||||
|
@ -18,6 +58,8 @@ const searchOpened = atomPairing(atom(false));
|
||||||
const settingsOpened = atomPairing(atom(false));
|
const settingsOpened = atomPairing(atom(false));
|
||||||
const subPanelOpened = atomPairing(atom(false));
|
const subPanelOpened = atomPairing(atom(false));
|
||||||
const mainPanelOpened = 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 menuGesturesEnabled = atomPairing(atomWithStorage("isMenuGesturesEnabled", false));
|
||||||
const terminalMode = atom((get) => get(settings.playerName[0]) === "root");
|
const terminalMode = atom((get) => get(settings.playerName[0]) === "root");
|
||||||
|
|
||||||
|
@ -29,23 +71,21 @@ const layout = {
|
||||||
mainPanelOpened,
|
mainPanelOpened,
|
||||||
menuGesturesEnabled,
|
menuGesturesEnabled,
|
||||||
terminalMode,
|
terminalMode,
|
||||||
};
|
debugMenuAvailable,
|
||||||
|
debugMenuOpened,
|
||||||
/* [ TERMINAL ATOMS ] */
|
|
||||||
|
|
||||||
const previousLines = atomPairing(atom<string[]>([]));
|
|
||||||
const previousCommands = atomPairing(atom<string[]>([]));
|
|
||||||
|
|
||||||
const terminal = {
|
|
||||||
previousLines,
|
|
||||||
previousCommands,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const atoms = {
|
export const atoms = {
|
||||||
settings,
|
settings,
|
||||||
layout,
|
layout,
|
||||||
terminal,
|
|
||||||
localData,
|
localData,
|
||||||
lightBox,
|
lightBox,
|
||||||
containerQueries,
|
containerQueries,
|
||||||
|
userAgent,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do not import outside of the "contexts" folder
|
||||||
|
export const internalAtoms = {
|
||||||
|
lightBox: lightBoxAtom,
|
||||||
|
localData: { languages, currencies, langui, fallbackLangui, recorders, typesTranslations },
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,42 +1,35 @@
|
||||||
import { atom } from "jotai";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useFetch } from "usehooks-ts";
|
import { useFetch } from "usehooks-ts";
|
||||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
import { useAtomSetter } from "helpers/atoms";
|
||||||
import {
|
|
||||||
Languages,
|
|
||||||
Currencies,
|
|
||||||
Langui,
|
|
||||||
processLangui,
|
|
||||||
processCurrencies,
|
|
||||||
processLanguages,
|
|
||||||
} from "helpers/localData";
|
|
||||||
import {
|
import {
|
||||||
LocalDataGetWebsiteInterfacesQuery,
|
LocalDataGetWebsiteInterfacesQuery,
|
||||||
LocalDataGetCurrenciesQuery,
|
LocalDataGetCurrenciesQuery,
|
||||||
LocalDataGetLanguagesQuery,
|
LocalDataGetLanguagesQuery,
|
||||||
|
LocalDataGetRecordersQuery,
|
||||||
} from "graphql/generated";
|
} from "graphql/generated";
|
||||||
import { LocalDataFile } from "graphql/fetchLocalData";
|
import { LocalDataFile } from "graphql/fetchLocalData";
|
||||||
|
import { internalAtoms } from "contexts/atoms";
|
||||||
const languages = atomPairing(atom<Languages>([]));
|
import {
|
||||||
const currencies = atomPairing(atom<Currencies>([]));
|
processLanguages,
|
||||||
const langui = atomPairing(atom<Langui>({}));
|
processCurrencies,
|
||||||
const fallbackLangui = atomPairing(atom<Langui>({}));
|
processLangui,
|
||||||
|
processRecorders,
|
||||||
export const localData = {
|
processTypesTranslations,
|
||||||
languages: languages[0],
|
} from "helpers/localData";
|
||||||
currencies: currencies[0],
|
import { getLogger } from "helpers/logger";
|
||||||
langui: langui[0],
|
|
||||||
fallbackLangui: fallbackLangui[0],
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
|
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
|
||||||
|
const logger = getLogger("💽 [Local Data]");
|
||||||
|
|
||||||
export const useLocalData = (): void => {
|
export const useLocalData = (): void => {
|
||||||
const setLanguages = useAtomSetter(languages);
|
const setLanguages = useAtomSetter(internalAtoms.localData.languages);
|
||||||
const setCurrencies = useAtomSetter(currencies);
|
const setCurrencies = useAtomSetter(internalAtoms.localData.currencies);
|
||||||
const setLangui = useAtomSetter(langui);
|
const setLangui = useAtomSetter(internalAtoms.localData.langui);
|
||||||
const setFallbackLangui = useAtomSetter(fallbackLangui);
|
const setFallbackLangui = useAtomSetter(internalAtoms.localData.fallbackLangui);
|
||||||
|
const setRecorders = useAtomSetter(internalAtoms.localData.recorders);
|
||||||
|
const setTypesTranslations = useAtomSetter(internalAtoms.localData.typesTranslations);
|
||||||
|
|
||||||
const { locale } = useRouter();
|
const { locale } = useRouter();
|
||||||
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
||||||
|
@ -44,24 +37,38 @@ export const useLocalData = (): void => {
|
||||||
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
||||||
getFileName("websiteInterfaces")
|
getFileName("websiteInterfaces")
|
||||||
);
|
);
|
||||||
|
const { data: rawRecorders } = useFetch<LocalDataGetRecordersQuery>(getFileName("recorders"));
|
||||||
|
const { data: rawTypesTranslations } = useFetch<LocalDataGetRecordersQuery>(
|
||||||
|
getFileName("typesTranslations")
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useLocalData] Refresh languages");
|
logger.log("Refresh languages");
|
||||||
setLanguages(processLanguages(rawLanguages));
|
setLanguages(processLanguages(rawLanguages));
|
||||||
}, [rawLanguages, setLanguages]);
|
}, [rawLanguages, setLanguages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useLocalData] Refresh currencies");
|
logger.log("Refresh currencies");
|
||||||
setCurrencies(processCurrencies(rawCurrencies));
|
setCurrencies(processCurrencies(rawCurrencies));
|
||||||
}, [rawCurrencies, setCurrencies]);
|
}, [rawCurrencies, setCurrencies]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useLocalData] Refresh langui");
|
logger.log("Refresh langui");
|
||||||
setLangui(processLangui(rawLangui, locale));
|
setLangui(processLangui(rawLangui, locale));
|
||||||
}, [locale, rawLangui, setLangui]);
|
}, [locale, rawLangui, setLangui]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useLocalData] Refresh fallback langui");
|
logger.log("Refresh fallback langui");
|
||||||
setFallbackLangui(processLangui(rawLangui, "en"));
|
setFallbackLangui(processLangui(rawLangui, "en"));
|
||||||
}, [rawLangui, setFallbackLangui]);
|
}, [rawLangui, setFallbackLangui]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logger.log("Refresh recorders");
|
||||||
|
setRecorders(processRecorders(rawRecorders));
|
||||||
|
}, [rawRecorders, setRecorders]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logger.log("Refresh types translations");
|
||||||
|
setTypesTranslations(processTypesTranslations(rawTypesTranslations));
|
||||||
|
}, [rawTypesTranslations, setTypesTranslations]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { useEffect } from "react";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { atomWithStorage } from "jotai/utils";
|
import { atomWithStorage } from "jotai/utils";
|
||||||
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
|
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||||
import { getDefaultPreferredLanguages } from "helpers/locales";
|
import { isDefined } from "helpers/asserts";
|
||||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
|
||||||
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
||||||
|
import { userAgent } from "contexts/userAgent";
|
||||||
|
import { getLogger } from "helpers/logger";
|
||||||
|
|
||||||
export enum ThemeMode {
|
export enum ThemeMode {
|
||||||
Dark = "dark",
|
Dark = "dark",
|
||||||
|
@ -13,13 +14,45 @@ export enum ThemeMode {
|
||||||
Light = "light",
|
Light = "light",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PerfMode {
|
||||||
|
On = "on",
|
||||||
|
Auto = "auto",
|
||||||
|
Off = "off",
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = getLogger("⚙️ [Settings Context]");
|
||||||
|
|
||||||
const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", []));
|
const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", []));
|
||||||
const themeModeAtom = atomPairing(atomWithStorage<ThemeMode>("themeMode", ThemeMode.Auto));
|
const themeModeAtom = atomPairing(atomWithStorage("themeMode", ThemeMode.Auto));
|
||||||
const darkModeAtom = atomPairing(atom(false));
|
const darkModeAtom = atomPairing(atom(false));
|
||||||
const fontSizeAtom = atomPairing(atomWithStorage("fontSize", 1));
|
const fontSizeAtom = atomPairing(atomWithStorage("fontSize", 1));
|
||||||
const dyslexicAtom = atomPairing(atomWithStorage("isDyslexic", false));
|
const dyslexicAtom = atomPairing(atomWithStorage("isDyslexic", false));
|
||||||
const currencyAtom = atomPairing(atomWithStorage("currency", "USD"));
|
const currencyAtom = atomPairing(atomWithStorage("currency", "USD"));
|
||||||
const playerNameAtom = atomPairing(atomWithStorage("playerName", ""));
|
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 = {
|
export const settings = {
|
||||||
preferredLanguages: preferredLanguagesAtom,
|
preferredLanguages: preferredLanguagesAtom,
|
||||||
|
@ -29,6 +62,9 @@ export const settings = {
|
||||||
dyslexic: dyslexicAtom,
|
dyslexic: dyslexicAtom,
|
||||||
currency: currencyAtom,
|
currency: currencyAtom,
|
||||||
playerName: playerNameAtom,
|
playerName: playerNameAtom,
|
||||||
|
perfMode: perfModeAtom,
|
||||||
|
isPerfModeEnabled: isPerfModeEnabledAtom,
|
||||||
|
isPerfModeToggleable: isPerfModeToggleableAtom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSettings = (): void => {
|
export const useSettings = (): void => {
|
||||||
|
@ -80,25 +116,33 @@ export const useSettings = (): void => {
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
/* PREFERRED LANGUAGES */
|
/* PREFERRED LANGUAGES */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preferredLanguages.length === 0) {
|
if (!router.locale || !router.locales) return;
|
||||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
const localStorageValue: string[] = JSON.parse(
|
||||||
console.log(router.locale, getDefaultPreferredLanguages(router.locale, router.locales));
|
localStorage.getItem("preferredLanguages") ?? "[]"
|
||||||
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
);
|
||||||
}
|
|
||||||
} else if (router.locale !== preferredLanguages[0]) {
|
if (localStorageValue.length === 0) {
|
||||||
/*
|
const defaultLanguages = router.locales;
|
||||||
* Using a timeout to the code getting stuck into a loop when reaching the website with a
|
defaultLanguages.sort((a, b) => {
|
||||||
* different preferredLanguages[0] from router.locale
|
const evaluate = (value: string) =>
|
||||||
*/
|
navigator.languages.includes(value)
|
||||||
setTimeout(
|
? navigator.languages.findIndex((v) => value === v)
|
||||||
async () =>
|
: navigator.languages.length;
|
||||||
router.replace(router.asPath, router.asPath, {
|
return evaluate(a) - evaluate(b);
|
||||||
locale: preferredLanguages[0],
|
});
|
||||||
}),
|
logger.log("First time visitor, initializing preferred languages to", defaultLanguages);
|
||||||
250
|
setPreferredLanguages(defaultLanguages);
|
||||||
|
} else if (router.locale !== localStorageValue[0]) {
|
||||||
|
logger.log(
|
||||||
|
"Router locale",
|
||||||
|
router.locale,
|
||||||
|
"doesn't correspond to preferred locale. Switching to",
|
||||||
|
localStorageValue[0]
|
||||||
);
|
);
|
||||||
|
router.replace(router.asPath, router.asPath, {
|
||||||
|
locale: localStorageValue[0],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [preferredLanguages, router, setPreferredLanguages]);
|
}, [router, setPreferredLanguages, preferredLanguages]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
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]);
|
||||||
|
};
|
|
@ -1,18 +0,0 @@
|
||||||
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,17 +3,33 @@ import { resolve } from "path";
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { getReadySdk } from "./sdk";
|
import { getReadySdk } from "./sdk";
|
||||||
import { LocalDataGetWebsiteInterfacesQuery } from "./generated";
|
import {
|
||||||
import { processLangui, Langui } from "helpers/localData";
|
LocalDataGetCurrenciesQuery,
|
||||||
|
LocalDataGetLanguagesQuery,
|
||||||
|
LocalDataGetTypesTranslationsQuery,
|
||||||
|
LocalDataGetWebsiteInterfacesQuery,
|
||||||
|
} from "./generated";
|
||||||
|
import {
|
||||||
|
processLangui,
|
||||||
|
Langui,
|
||||||
|
TypesTranslations,
|
||||||
|
processTypesTranslations,
|
||||||
|
Currencies,
|
||||||
|
processCurrencies,
|
||||||
|
Languages,
|
||||||
|
processLanguages,
|
||||||
|
} from "helpers/localData";
|
||||||
|
import { getLogger } from "helpers/logger";
|
||||||
|
|
||||||
config({ path: resolve(process.cwd(), ".env.local") });
|
config({ path: resolve(process.cwd(), ".env.local") });
|
||||||
|
|
||||||
const LOCAL_DATA_FOLDER = `${process.cwd()}/public/local-data`;
|
const LOCAL_DATA_FOLDER = `${process.cwd()}/public/local-data`;
|
||||||
|
const logger = getLogger("💽 [Local Data]", "server");
|
||||||
|
|
||||||
const writeLocalData = (name: LocalDataFile, localData: unknown) => {
|
const writeLocalData = (name: LocalDataFile, localData: object) => {
|
||||||
const path = `${LOCAL_DATA_FOLDER}/${name}.json`;
|
const path = `${LOCAL_DATA_FOLDER}/${name}.json`;
|
||||||
writeFileSync(path, JSON.stringify(localData), { encoding: "utf-8" });
|
writeFileSync(path, JSON.stringify(localData), { encoding: "utf-8" });
|
||||||
console.log(`${path} has been written!`);
|
logger.log(`${name}.json has been written`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const readLocalData = <T>(name: LocalDataFile): T => {
|
const readLocalData = <T>(name: LocalDataFile): T => {
|
||||||
|
@ -21,22 +37,68 @@ const readLocalData = <T>(name: LocalDataFile): T => {
|
||||||
return JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
return JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchLocalData = async (): Promise<void> => {
|
export const fetchWebsiteInterfaces = async (): Promise<void> => {
|
||||||
const sdk = getReadySdk();
|
const sdk = getReadySdk();
|
||||||
writeLocalData("websiteInterfaces", await sdk.localDataGetWebsiteInterfaces());
|
writeLocalData("websiteInterfaces", await sdk.localDataGetWebsiteInterfaces());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchCurrencies = async (): Promise<void> => {
|
||||||
|
const sdk = getReadySdk();
|
||||||
writeLocalData("currencies", await sdk.localDataGetCurrencies());
|
writeLocalData("currencies", await sdk.localDataGetCurrencies());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchLanguages = async (): Promise<void> => {
|
||||||
|
const sdk = getReadySdk();
|
||||||
writeLocalData("languages", await sdk.localDataGetLanguages());
|
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") {
|
if (process.argv[2] === "--esrun") {
|
||||||
fetchLocalData();
|
fetchLocalData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
export type LocalDataFile = "currencies" | "languages" | "websiteInterfaces";
|
export type LocalDataFile =
|
||||||
|
| "currencies"
|
||||||
|
| "languages"
|
||||||
|
| "recorders"
|
||||||
|
| "typesTranslations"
|
||||||
|
| "websiteInterfaces";
|
||||||
|
|
||||||
export const getLangui = (locale: string | undefined): Langui => {
|
export const getLangui = (locale: string): Langui => {
|
||||||
const websiteInterfaces = readLocalData<LocalDataGetWebsiteInterfacesQuery>("websiteInterfaces");
|
const websiteInterfaces = readLocalData<LocalDataGetWebsiteInterfacesQuery>("websiteInterfaces");
|
||||||
return processLangui(websiteInterfaces, locale);
|
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);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
fragment parentFolderPreview on ContentsFolder {
|
||||||
|
slug
|
||||||
|
titles(pagination: { limit: -1 }) {
|
||||||
|
id
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
fragment relatedContentPreview on Content {
|
||||||
|
slug
|
||||||
|
translations(pagination: { limit: -1 }) {
|
||||||
|
pre_title
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ import { GetStaticProps } from "next";
|
||||||
import { getReadySdk } from "./sdk";
|
import { getReadySdk } from "./sdk";
|
||||||
import { PostWithTranslations } from "types/types";
|
import { PostWithTranslations } from "types/types";
|
||||||
import { getOpenGraph } from "helpers/openGraph";
|
import { getOpenGraph } from "helpers/openGraph";
|
||||||
import { prettyDate, prettySlug } from "helpers/formatters";
|
import { prettySlug } from "helpers/formatters";
|
||||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
import { filterHasAttributes } from "helpers/asserts";
|
||||||
import { getDescription } from "helpers/description";
|
import { getDescription } from "helpers/description";
|
||||||
import { AppLayoutRequired } from "components/AppLayout";
|
import { AppLayoutRequired } from "components/AppLayout";
|
||||||
import { getFormat } from "helpers/i18n";
|
import { getFormat } from "helpers/i18n";
|
||||||
|
@ -17,45 +17,40 @@ export const getPostStaticProps =
|
||||||
(slug: string): GetStaticProps =>
|
(slug: string): GetStaticProps =>
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const sdk = getReadySdk();
|
const sdk = getReadySdk();
|
||||||
const { format } = getFormat(context.locale);
|
const { format, formatCategory, formatDate } = getFormat(context.locale);
|
||||||
const post = await sdk.getPost({
|
const post = await sdk.getPost({
|
||||||
slug: slug,
|
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
const title = selectedTranslation?.title ?? prettySlug(slug);
|
if (!post.posts?.data[0]?.attributes?.translations || !context.locale || !context.locales) {
|
||||||
|
return { notFound: true };
|
||||||
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"] as const
|
|
||||||
).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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { notFound: true };
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,6 @@ export interface ICUParams {
|
||||||
category: { count: number };
|
category: { count: number };
|
||||||
size: never;
|
size: never;
|
||||||
release_date: never;
|
release_date: never;
|
||||||
release_year: never;
|
|
||||||
details: never;
|
details: never;
|
||||||
price: never;
|
price: never;
|
||||||
width: never;
|
width: never;
|
||||||
|
@ -41,13 +40,9 @@ export interface ICUParams {
|
||||||
front_matter: never;
|
front_matter: never;
|
||||||
back_matter: never;
|
back_matter: never;
|
||||||
open_content: never;
|
open_content: never;
|
||||||
read_content: never;
|
|
||||||
watch_content: never;
|
|
||||||
listen_content: never;
|
|
||||||
view_scans: never;
|
view_scans: never;
|
||||||
paperback: never;
|
paperback: never;
|
||||||
hardcover: never;
|
hardcover: never;
|
||||||
select_language: never;
|
|
||||||
language: { count: number };
|
language: { count: number };
|
||||||
library_description: never;
|
library_description: never;
|
||||||
wiki_description: never;
|
wiki_description: never;
|
||||||
|
@ -62,7 +57,6 @@ export interface ICUParams {
|
||||||
show_primary_items: never;
|
show_primary_items: never;
|
||||||
show_secondary_items: never;
|
show_secondary_items: never;
|
||||||
order_by: never;
|
order_by: never;
|
||||||
group_by: never;
|
|
||||||
select_option_sidebar: never;
|
select_option_sidebar: never;
|
||||||
group: never;
|
group: never;
|
||||||
settings: never;
|
settings: never;
|
||||||
|
@ -84,17 +78,12 @@ export interface ICUParams {
|
||||||
review: never;
|
review: never;
|
||||||
done: never;
|
done: never;
|
||||||
status: never;
|
status: never;
|
||||||
transcribers: never;
|
|
||||||
translators: never;
|
|
||||||
proofreaders: never;
|
|
||||||
transcript_notice: never;
|
transcript_notice: never;
|
||||||
translation_notice: never;
|
translation_notice: never;
|
||||||
source_language: never;
|
source_language: never;
|
||||||
pronouns: never;
|
pronouns: never;
|
||||||
item: { count: number };
|
item: { count: number };
|
||||||
content: never;
|
|
||||||
open_settings: never;
|
open_settings: never;
|
||||||
change_language: never;
|
|
||||||
open_search: never;
|
open_search: never;
|
||||||
chronology: never;
|
chronology: never;
|
||||||
accords_handbook: never;
|
accords_handbook: never;
|
||||||
|
@ -112,16 +101,14 @@ export interface ICUParams {
|
||||||
item_not_available: never;
|
item_not_available: never;
|
||||||
primary_language: never;
|
primary_language: never;
|
||||||
secondary_language: never;
|
secondary_language: never;
|
||||||
combine_related_contents: never;
|
previous_content: { count: number };
|
||||||
previous_content: never;
|
followup_content: { count: number };
|
||||||
followup_content: never;
|
|
||||||
videos: never;
|
videos: never;
|
||||||
view_on: never;
|
view_on_x: { x: Date | boolean | number | string };
|
||||||
channel: never;
|
channel: never;
|
||||||
subscribers: never;
|
subscribers: never;
|
||||||
description: never;
|
description: never;
|
||||||
available_at: never;
|
available_at_x: { x: Date | boolean | number | string };
|
||||||
search_title: never;
|
|
||||||
want_it: never;
|
want_it: never;
|
||||||
have_it: never;
|
have_it: never;
|
||||||
source: never;
|
source: never;
|
||||||
|
@ -129,7 +116,6 @@ export interface ICUParams {
|
||||||
only_display_items_i_have: never;
|
only_display_items_i_have: never;
|
||||||
only_display_items_i_want: never;
|
only_display_items_i_want: never;
|
||||||
only_display_unmarked_items: never;
|
only_display_unmarked_items: never;
|
||||||
display_all_items: never;
|
|
||||||
table_of_contents: never;
|
table_of_contents: never;
|
||||||
no_results_message: never;
|
no_results_message: never;
|
||||||
all: never;
|
all: never;
|
||||||
|
@ -140,7 +126,6 @@ export interface ICUParams {
|
||||||
cleaners: never;
|
cleaners: never;
|
||||||
typesetters: never;
|
typesetters: never;
|
||||||
notes: never;
|
notes: never;
|
||||||
cover: never;
|
|
||||||
tags: never;
|
tags: never;
|
||||||
no_source_warning: never;
|
no_source_warning: never;
|
||||||
copy_anchor_link: never;
|
copy_anchor_link: never;
|
||||||
|
@ -149,7 +134,6 @@ export interface ICUParams {
|
||||||
empty_folder_message: never;
|
empty_folder_message: never;
|
||||||
switch_to_grid_view: never;
|
switch_to_grid_view: never;
|
||||||
switch_to_folder_view: never;
|
switch_to_folder_view: never;
|
||||||
content_is_not_available: never;
|
|
||||||
paper_texture: never;
|
paper_texture: never;
|
||||||
book_fold: never;
|
book_fold: never;
|
||||||
lighting: never;
|
lighting: never;
|
||||||
|
@ -177,5 +161,20 @@ export interface ICUParams {
|
||||||
x_results: { x: number };
|
x_results: { x: number };
|
||||||
definition_x: { x: Date | boolean | number | string };
|
definition_x: { x: Date | boolean | number | string };
|
||||||
subitem_of_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 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,17 @@
|
||||||
import { createWriteStream } from "fs";
|
import { createWriteStream } from "fs";
|
||||||
import { parse, TYPE } from "@formatjs/icu-messageformat-parser";
|
import { parse, TYPE } from "@formatjs/icu-messageformat-parser";
|
||||||
import { getLangui } from "./fetchLocalData";
|
import { getLangui } from "./fetchLocalData";
|
||||||
import { filterDefined } from "helpers/asserts";
|
import { getLogger } from "helpers/logger";
|
||||||
|
|
||||||
const OUTPUT_FOLDER = `${process.cwd()}/src/graphql`;
|
const OUTPUT_FOLDER = `${process.cwd()}/src/graphql`;
|
||||||
|
const logger = getLogger("💽 [ICU to TS]", "server");
|
||||||
|
|
||||||
|
const isDefined = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;
|
||||||
|
|
||||||
|
const isUndefined = <T>(t: T | null | undefined): t is null | undefined => !isDefined(t);
|
||||||
|
|
||||||
|
const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
|
||||||
|
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
|
||||||
|
|
||||||
const icuToTypescript = () => {
|
const icuToTypescript = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@ -45,7 +53,7 @@ const icuToTypescript = () => {
|
||||||
|
|
||||||
output.write("}\n");
|
output.write("}\n");
|
||||||
|
|
||||||
console.log(`${OUTPUT_FOLDER}/icu-params.ts has been written!`);
|
logger.log(`icu-params.ts has been written!`);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.argv[2] === "--icu") {
|
if (process.argv[2] === "--icu") {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
query getChronicle($slug: String, $language_code: String) {
|
query getChronicle($slug: String) {
|
||||||
chronicles(filters: { slug: { eq: $slug } }) {
|
chronicles(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
|
@ -53,21 +53,21 @@ query getChronicle($slug: String, $language_code: String) {
|
||||||
authors {
|
authors {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
translators {
|
translators {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proofreaders {
|
proofreaders {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,10 +80,8 @@ query getChronicle($slug: String, $language_code: String) {
|
||||||
slug
|
slug
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,9 +89,6 @@ query getChronicle($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +118,7 @@ query getChronicle($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,7 +126,7 @@ query getChronicle($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +134,7 @@ query getChronicle($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
query getContentText($slug: String, $language_code: String) {
|
query getContentText($slug: String) {
|
||||||
contents(filters: { slug: { eq: $slug } }) {
|
contents(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
|
@ -6,10 +6,8 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
slug
|
slug
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +15,6 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,10 +48,8 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,19 +60,15 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on ComponentMetadataGame {
|
... on ComponentMetadataGame {
|
||||||
platforms(pagination: { limit: -1 }) {
|
platform {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
short
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,9 +78,6 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,9 +87,6 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,9 +96,6 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,9 +103,6 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +140,7 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +148,7 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +156,44 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,51 +222,20 @@ query getContentText($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
sequence
|
}
|
||||||
contents(pagination: { limit: -1 }) {
|
}
|
||||||
data {
|
}
|
||||||
attributes {
|
previous_contents(pagination: { limit: -1 }) {
|
||||||
slug
|
data {
|
||||||
translations(pagination: { limit: -1 }) {
|
attributes {
|
||||||
pre_title
|
...relatedContentPreview
|
||||||
title
|
}
|
||||||
subtitle
|
}
|
||||||
language {
|
}
|
||||||
data {
|
next_contents(pagination: { limit: -1 }) {
|
||||||
attributes {
|
data {
|
||||||
code
|
attributes {
|
||||||
}
|
...relatedContentPreview
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
categories(pagination: { limit: -1 }) {
|
|
||||||
data {
|
|
||||||
id
|
|
||||||
attributes {
|
|
||||||
short
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type {
|
|
||||||
data {
|
|
||||||
attributes {
|
|
||||||
slug
|
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbnail {
|
|
||||||
data {
|
|
||||||
attributes {
|
|
||||||
...uploadImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,8 @@
|
||||||
query getContentsFolder($slug: String, $language_code: String) {
|
query getContentsFolder($slug: String) {
|
||||||
contentsFolders(filters: { slug: { eq: $slug } }) {
|
contentsFolders(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
...parentFolderPreview
|
||||||
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 }) {
|
contents(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
|
@ -51,10 +22,8 @@ query getContentsFolder($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,9 +31,6 @@ query getContentsFolder($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,6 +90,55 @@ query getContentsFolder($slug: String, $language_code: 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, $language_code: String) {
|
query getLibraryItem($slug: String) {
|
||||||
libraryItems(filters: { slug: { eq: $slug } }) {
|
libraryItems(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
|
@ -9,6 +9,7 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
root_item
|
root_item
|
||||||
primary
|
primary
|
||||||
digital
|
digital
|
||||||
|
download_available
|
||||||
thumbnail {
|
thumbnail {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
|
@ -32,10 +33,8 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,9 +56,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,19 +76,15 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on ComponentMetadataGame {
|
... on ComponentMetadataGame {
|
||||||
platforms(pagination: { limit: -1 }) {
|
platform {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
short
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,21 +118,20 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tracks(pagination: { limit: -1 }) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
... on ComponentMetadataGroup {
|
... on ComponentMetadataGroup {
|
||||||
subtype {
|
subtype {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,9 +139,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,10 +176,8 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,19 +188,16 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on ComponentMetadataGame {
|
... on ComponentMetadataGame {
|
||||||
platforms {
|
platform {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
short
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,9 +207,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,9 +216,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,9 +225,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,9 +232,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,10 +282,8 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
slug
|
slug
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -322,9 +291,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
query getLibraryItemScans($slug: String, $language_code: String) {
|
query getLibraryItemScans($slug: String) {
|
||||||
libraryItems(filters: { slug: { eq: $slug } }) {
|
libraryItems(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
|
@ -6,6 +6,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
slug
|
slug
|
||||||
title
|
title
|
||||||
subtitle
|
subtitle
|
||||||
|
download_available
|
||||||
images(pagination: { limit: -1 }) {
|
images(pagination: { limit: -1 }) {
|
||||||
status
|
status
|
||||||
language {
|
language {
|
||||||
|
@ -26,7 +27,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +35,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +43,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,10 +156,8 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,19 +169,16 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on ComponentMetadataGame {
|
... on ComponentMetadataGame {
|
||||||
platforms {
|
platform {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
short
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,9 +188,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,9 +197,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,9 +206,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,9 +213,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,7 +273,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,7 +281,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -305,7 +289,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
query getPost($slug: String, $language_code: String) {
|
query getPost($slug: String) {
|
||||||
posts(filters: { slug: { eq: $slug } }) {
|
posts(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
|
@ -12,16 +12,14 @@ query getPost($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,8 @@ query getVideo($uid: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
short
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
query getWeapon($slug: String) {
|
||||||
|
weaponStories(filters: { slug: { eq: $slug } }) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...sharedWeaponFragment
|
||||||
|
stories(pagination: { limit: -1 }) {
|
||||||
|
id
|
||||||
|
categories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
translations(pagination: { limit: -1 }) {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
level_1
|
||||||
|
level_2
|
||||||
|
level_3
|
||||||
|
level_4
|
||||||
|
status
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weapon_group {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
weapons(pagination: { limit: -1 }, filters: { slug: { ne: $slug } }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...sharedWeaponFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment sharedWeaponFragment on WeaponStory {
|
||||||
|
type {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name(pagination: { limit: -1 }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slug
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getWeaponsSlugs {
|
||||||
|
weaponStories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
query getWikiPage($slug: String, $language_code: String) {
|
query getWikiPage($slug: String) {
|
||||||
wikiPages(filters: { slug: { eq: $slug } }) {
|
wikiPages(filters: { slug: { eq: $slug } }) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
|
@ -13,21 +13,15 @@ query getWikiPage($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
slug
|
slug
|
||||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +52,7 @@ query getWikiPage($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +60,7 @@ query getWikiPage($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +68,7 @@ query getWikiPage($slug: String, $language_code: String) {
|
||||||
data {
|
data {
|
||||||
id
|
id
|
||||||
attributes {
|
attributes {
|
||||||
...recorderChip
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,10 +84,8 @@ query getWikiPage($slug: String, $language_code: String) {
|
||||||
}
|
}
|
||||||
categories(pagination: { limit: -1 }) {
|
categories(pagination: { limit: -1 }) {
|
||||||
data {
|
data {
|
||||||
id
|
|
||||||
attributes {
|
attributes {
|
||||||
name
|
slug
|
||||||
short
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
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,7 +28,6 @@ query localDataGetWebsiteInterfaces {
|
||||||
category
|
category
|
||||||
size
|
size
|
||||||
release_date
|
release_date
|
||||||
release_year
|
|
||||||
details
|
details
|
||||||
price
|
price
|
||||||
width
|
width
|
||||||
|
@ -51,13 +50,9 @@ query localDataGetWebsiteInterfaces {
|
||||||
front_matter
|
front_matter
|
||||||
back_matter
|
back_matter
|
||||||
open_content
|
open_content
|
||||||
read_content
|
|
||||||
watch_content
|
|
||||||
listen_content
|
|
||||||
view_scans
|
view_scans
|
||||||
paperback
|
paperback
|
||||||
hardcover
|
hardcover
|
||||||
select_language
|
|
||||||
language
|
language
|
||||||
library_description
|
library_description
|
||||||
wiki_description
|
wiki_description
|
||||||
|
@ -72,7 +67,6 @@ query localDataGetWebsiteInterfaces {
|
||||||
show_primary_items
|
show_primary_items
|
||||||
show_secondary_items
|
show_secondary_items
|
||||||
order_by
|
order_by
|
||||||
group_by
|
|
||||||
select_option_sidebar
|
select_option_sidebar
|
||||||
group
|
group
|
||||||
settings
|
settings
|
||||||
|
@ -94,17 +88,12 @@ query localDataGetWebsiteInterfaces {
|
||||||
review
|
review
|
||||||
done
|
done
|
||||||
status
|
status
|
||||||
transcribers
|
|
||||||
translators
|
|
||||||
proofreaders
|
|
||||||
transcript_notice
|
transcript_notice
|
||||||
translation_notice
|
translation_notice
|
||||||
source_language
|
source_language
|
||||||
pronouns
|
pronouns
|
||||||
item
|
item
|
||||||
content
|
|
||||||
open_settings
|
open_settings
|
||||||
change_language
|
|
||||||
open_search
|
open_search
|
||||||
chronology
|
chronology
|
||||||
accords_handbook
|
accords_handbook
|
||||||
|
@ -122,16 +111,14 @@ query localDataGetWebsiteInterfaces {
|
||||||
item_not_available
|
item_not_available
|
||||||
primary_language
|
primary_language
|
||||||
secondary_language
|
secondary_language
|
||||||
combine_related_contents
|
|
||||||
previous_content
|
previous_content
|
||||||
followup_content
|
followup_content
|
||||||
videos
|
videos
|
||||||
view_on
|
view_on_x
|
||||||
channel
|
channel
|
||||||
subscribers
|
subscribers
|
||||||
description
|
description
|
||||||
available_at
|
available_at_x
|
||||||
search_title
|
|
||||||
want_it
|
want_it
|
||||||
have_it
|
have_it
|
||||||
source
|
source
|
||||||
|
@ -139,7 +126,6 @@ query localDataGetWebsiteInterfaces {
|
||||||
only_display_items_i_have
|
only_display_items_i_have
|
||||||
only_display_items_i_want
|
only_display_items_i_want
|
||||||
only_display_unmarked_items
|
only_display_unmarked_items
|
||||||
display_all_items
|
|
||||||
table_of_contents
|
table_of_contents
|
||||||
no_results_message
|
no_results_message
|
||||||
all
|
all
|
||||||
|
@ -150,7 +136,6 @@ query localDataGetWebsiteInterfaces {
|
||||||
cleaners
|
cleaners
|
||||||
typesetters
|
typesetters
|
||||||
notes
|
notes
|
||||||
cover
|
|
||||||
tags
|
tags
|
||||||
no_source_warning
|
no_source_warning
|
||||||
copy_anchor_link
|
copy_anchor_link
|
||||||
|
@ -159,7 +144,6 @@ query localDataGetWebsiteInterfaces {
|
||||||
empty_folder_message
|
empty_folder_message
|
||||||
switch_to_grid_view
|
switch_to_grid_view
|
||||||
switch_to_folder_view
|
switch_to_folder_view
|
||||||
content_is_not_available
|
|
||||||
paper_texture
|
paper_texture
|
||||||
book_fold
|
book_fold
|
||||||
lighting
|
lighting
|
||||||
|
@ -184,7 +168,22 @@ query localDataGetWebsiteInterfaces {
|
||||||
x_results
|
x_results
|
||||||
definition_x
|
definition_x
|
||||||
subitem_of_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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
query revalidationGetWeaponGroup($id: ID) {
|
||||||
|
weaponStoryGroup(id: $id) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
weapons(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* 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,10 +1,19 @@
|
||||||
export const sendAnalytics = (category: string, event: string): void => {
|
import { getLogger } from "helpers/logger";
|
||||||
|
import { TrackingFunction } from "types/global";
|
||||||
|
|
||||||
|
const logger = getLogger("📊 [Analytics]");
|
||||||
|
|
||||||
|
export const sendAnalytics = (
|
||||||
|
category: string,
|
||||||
|
event: Parameters<TrackingFunction>[0],
|
||||||
|
data?: Parameters<TrackingFunction>[1]
|
||||||
|
): void => {
|
||||||
const eventName = `[${category}] ${event}`;
|
const eventName = `[${category}] ${event}`;
|
||||||
console.log(`Event: ${eventName}`);
|
logger.log(eventName);
|
||||||
try {
|
try {
|
||||||
umami(eventName);
|
umami.track(eventName, data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ReferenceError) return;
|
if (error instanceof ReferenceError) return;
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const isDefinedAndNotEmpty = (string: string | null | undefined): string
|
||||||
export const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
|
export const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
|
||||||
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
|
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
|
||||||
|
|
||||||
export const filterHasAttributes = <T, P extends PathDot<T>>(
|
export const filterHasAttributes = <T, const P extends PathDot<T>>(
|
||||||
t: T[] | null | undefined,
|
t: T[] | null | undefined,
|
||||||
paths: readonly P[]
|
paths: readonly P[]
|
||||||
): SelectiveNonNullable<T, (typeof paths)[number]>[] =>
|
): SelectiveNonNullable<T, (typeof paths)[number]>[] =>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { atom, PrimitiveAtom, Atom, WritableAtom, useAtom } from "jotai";
|
import { atom, PrimitiveAtom, Atom, WritableAtom, useAtom } from "jotai";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
type AtomPair<T> = [Atom<T>, WritableAtom<null, T>];
|
type AtomPair<T> = [Atom<T>, WritableAtom<null, [newText: T], void>];
|
||||||
|
|
||||||
export const atomPairing = <T>(anAtom: PrimitiveAtom<T>): AtomPair<T> => {
|
export const atomPairing = <T>(anAtom: PrimitiveAtom<T>): AtomPair<T> => {
|
||||||
const getter = atom((get) => get(anAtom));
|
const getter = atom((get) => get(anAtom));
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { isDefined } from "./asserts";
|
import { ReactNode, useMemo } from "react";
|
||||||
|
import { HorizontalLine } from "components/HorizontalLine";
|
||||||
|
import { insertInBetweenArray } from "helpers/others";
|
||||||
|
import { isDefined } from "helpers/asserts";
|
||||||
|
|
||||||
export interface Wrapper {
|
export interface Wrapper {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -28,3 +31,33 @@ export const ConditionalWrapper = <T, U>({
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface ElementsSeparatorProps {
|
||||||
|
children: React.ReactNode[];
|
||||||
|
separator?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ElementsSeparator = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
separator = <HorizontalLine className={className} />,
|
||||||
|
}: 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,4 +1,5 @@
|
||||||
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "./asserts";
|
import { prettyMarkdown } from "helpers/formatters";
|
||||||
|
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||||
|
|
||||||
export const getDescription = (
|
export const getDescription = (
|
||||||
description: string | null | undefined,
|
description: string | null | undefined,
|
||||||
|
@ -6,13 +7,6 @@ export const getDescription = (
|
||||||
): string => {
|
): string => {
|
||||||
let result = "";
|
let result = "";
|
||||||
|
|
||||||
if (isDefinedAndNotEmpty(description)) {
|
|
||||||
result += prettyMarkdown(description);
|
|
||||||
if (isDefined(chipsGroups)) {
|
|
||||||
result += "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in chipsGroups) {
|
for (const key in chipsGroups) {
|
||||||
if (Object.hasOwn(chipsGroups, key)) {
|
if (Object.hasOwn(chipsGroups, key)) {
|
||||||
const chipsGroup = filterDefined(chipsGroups[key]);
|
const chipsGroup = filterDefined(chipsGroups[key]);
|
||||||
|
@ -22,12 +16,16 @@ export const getDescription = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefinedAndNotEmpty(description)) {
|
||||||
|
if (result !== "") {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
result += prettyMarkdown(description);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prettyMarkdown = (markdown: string): string =>
|
|
||||||
markdown.replace(/[*]/gu, "").replace(/[_]/gu, "");
|
|
||||||
|
|
||||||
const prettyChip = (items: (string | undefined)[]): string =>
|
const prettyChip = (items: (string | undefined)[]): string =>
|
||||||
items
|
items
|
||||||
.filter((item) => isDefined(item))
|
.filter((item) => isDefined(item))
|
||||||
|
|
|
@ -1,39 +1,7 @@
|
||||||
import { convertPrice } from "./numbers";
|
import { convert } from "html-to-text";
|
||||||
import { isDefinedAndNotEmpty, isUndefined } from "./asserts";
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
import { datePickerToDate } from "./date";
|
import { Renderer, marked } from "marked";
|
||||||
import { Currencies, Languages } from "./localData";
|
import { isDefinedAndNotEmpty } from "./asserts";
|
||||||
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 => {
|
export const prettySlug = (slug?: string, parentSlug?: string): string => {
|
||||||
let newSlug = slug;
|
let newSlug = slug;
|
||||||
|
@ -58,136 +26,6 @@ export const prettyInlineTitle = (
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable id-denylist */
|
|
||||||
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 => {
|
export const prettyShortenNumber = (number: number): string => {
|
||||||
if (number > 1_000_000) {
|
if (number > 1_000_000) {
|
||||||
return `${(number / 1_000_000).toLocaleString(undefined, {
|
return `${(number / 1_000_000).toLocaleString(undefined, {
|
||||||
|
@ -216,15 +54,7 @@ export const prettyDuration = (seconds: number): string => {
|
||||||
let result = "";
|
let result = "";
|
||||||
if (hours) result += `${hours.toString().padStart(2, "0")}:`;
|
if (hours) result += `${hours.toString().padStart(2, "0")}:`;
|
||||||
result += `${minutes.toString().padStart(2, "0")}:`;
|
result += `${minutes.toString().padStart(2, "0")}:`;
|
||||||
result += remainingSeconds.toString().padStart(2, "0");
|
result += Math.floor(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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -262,3 +92,41 @@ export const slugify = (string: string | undefined): string => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sJoin = (...args: (string | null | undefined)[]): string => args.join("");
|
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,13 +1,24 @@
|
||||||
import { IntlMessageFormat } from "intl-messageformat";
|
import { IntlMessageFormat } from "intl-messageformat";
|
||||||
import { LibraryItemMetadataDynamicZone } from "graphql/generated";
|
import {
|
||||||
|
DatePickerFragment,
|
||||||
|
LibraryItemMetadataDynamicZone,
|
||||||
|
PricePickerFragment,
|
||||||
|
} from "graphql/generated";
|
||||||
import { ICUParams } from "graphql/icuParams";
|
import { ICUParams } from "graphql/icuParams";
|
||||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
|
||||||
import { getLangui } from "graphql/fetchLocalData";
|
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";
|
||||||
|
|
||||||
type WordingKey = keyof ICUParams;
|
type WordingKey = keyof ICUParams;
|
||||||
|
|
||||||
type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>;
|
type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>;
|
||||||
|
|
||||||
type ContentStatus = "Done" | "Draft" | "Incomplete" | "Review";
|
type ContentStatus = "Done" | "Draft" | "Incomplete" | "Review";
|
||||||
|
|
||||||
const componentMetadataToWording: Record<LibraryItemType, WordingKey> = {
|
const componentMetadataToWording: Record<LibraryItemType, WordingKey> = {
|
||||||
|
@ -31,18 +42,32 @@ const componentSetsTextsetStatusToWording: Record<
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormat = (
|
export const getFormat = (
|
||||||
locale: string | undefined
|
locale: string | undefined = "en"
|
||||||
): {
|
): {
|
||||||
format: <K extends WordingKey>(
|
format: <K extends WordingKey>(
|
||||||
key: K,
|
key: K,
|
||||||
...values: ICUParams[K] extends never ? [undefined?] : [ICUParams[K]]
|
...values: ICUParams[K] extends never ? [undefined?] : [ICUParams[K]]
|
||||||
) => string;
|
) => string;
|
||||||
formatLibraryItemType: (metadata: { __typename: LibraryItemType }) => string;
|
formatLibraryItemType: (metadata: LibraryItemMetadata) => string;
|
||||||
|
formatLibraryItemSubType: (metadata: LibraryItemMetadata) => string;
|
||||||
formatStatusLabel: (status: ContentStatus) => string;
|
formatStatusLabel: (status: ContentStatus) => string;
|
||||||
formatStatusDescription: (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 langui = getLangui(locale);
|
||||||
const fallbackLangui = getLangui("en");
|
const fallbackLangui = getLangui("en");
|
||||||
|
const typesTranslations = getTypesTranslations();
|
||||||
|
const currencies = getCurrencies();
|
||||||
|
const languages = getLanguages();
|
||||||
|
|
||||||
const format = (
|
const format = (
|
||||||
key: WordingKey,
|
key: WordingKey,
|
||||||
|
@ -58,11 +83,99 @@ export const getFormat = (
|
||||||
if (isDefinedAndNotEmpty(result)) {
|
if (isDefinedAndNotEmpty(result)) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return new IntlMessageFormat(fallbackLangui[key] ?? "").format(processedValues).toString();
|
const fallback = new IntlMessageFormat(fallbackLangui[key] ?? "")
|
||||||
|
.format(processedValues)
|
||||||
|
.toString();
|
||||||
|
if (isDefinedAndNotEmpty(fallback)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLibraryItemType = (metadata: { __typename: LibraryItemType }): string =>
|
const formatLibraryItemType = (metadata: LibraryItemMetadata): string =>
|
||||||
format(componentMetadataToWording[metadata.__typename]);
|
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 formatStatusLabel = (status: ContentStatus): string =>
|
const formatStatusLabel = (status: ContentStatus): string =>
|
||||||
format(componentSetsTextsetStatusToWording[status].label);
|
format(componentSetsTextsetStatusToWording[status].label);
|
||||||
|
@ -70,10 +183,97 @@ export const getFormat = (
|
||||||
const formatStatusDescription = (status: ContentStatus): string =>
|
const formatStatusDescription = (status: ContentStatus): string =>
|
||||||
format(componentSetsTextsetStatusToWording[status].description);
|
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 {
|
return {
|
||||||
format,
|
format,
|
||||||
formatLibraryItemType,
|
formatLibraryItemType,
|
||||||
|
formatLibraryItemSubType,
|
||||||
formatStatusLabel,
|
formatStatusLabel,
|
||||||
formatStatusDescription,
|
formatStatusDescription,
|
||||||
|
formatCategory,
|
||||||
|
formatContentType,
|
||||||
|
formatWikiTag,
|
||||||
|
formatWeaponType,
|
||||||
|
formatLanguage,
|
||||||
|
formatPrice,
|
||||||
|
formatDate,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,6 @@ export const isUntangibleGroupItem = (
|
||||||
metadata:
|
metadata:
|
||||||
| {
|
| {
|
||||||
__typename: string;
|
__typename: string;
|
||||||
// eslint-disable-next-line id-denylist
|
|
||||||
subtype?: { data?: { attributes?: { slug: string } | null } | null } | null;
|
subtype?: { data?: { attributes?: { slug: string } | null } | null } | null;
|
||||||
}
|
}
|
||||||
| null
|
| null
|
||||||
|
@ -14,3 +13,9 @@ export const isUntangibleGroupItem = (
|
||||||
metadata.__typename === "ComponentMetadataGroup" &&
|
metadata.__typename === "ComponentMetadataGroup" &&
|
||||||
(metadata.subtype?.data?.attributes?.slug === "variant-set" ||
|
(metadata.subtype?.data?.attributes?.slug === "variant-set" ||
|
||||||
metadata.subtype?.data?.attributes?.slug === "relation-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,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
LocalDataGetCurrenciesQuery,
|
LocalDataGetCurrenciesQuery,
|
||||||
LocalDataGetLanguagesQuery,
|
LocalDataGetLanguagesQuery,
|
||||||
|
LocalDataGetRecordersQuery,
|
||||||
|
LocalDataGetTypesTranslationsQuery,
|
||||||
LocalDataGetWebsiteInterfacesQuery,
|
LocalDataGetWebsiteInterfacesQuery,
|
||||||
} from "graphql/generated";
|
} from "graphql/generated";
|
||||||
|
|
||||||
|
@ -45,3 +47,58 @@ export const processLanguages = (languages: LocalDataGetLanguagesQuery | undefin
|
||||||
}
|
}
|
||||||
return languages?.languages?.data ?? [];
|
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 ?? [],
|
||||||
|
});
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue