Compare commits
87 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
e88345f395 | |
![]() |
34c4570688 | |
![]() |
da916f898a | |
![]() |
7efa43a630 | |
![]() |
22e1bf4842 | |
![]() |
d560008cff | |
![]() |
872f31a6a3 | |
![]() |
3c7b9aa2d6 | |
![]() |
62e64b9319 | |
![]() |
e0ee70814d | |
![]() |
87625ba9ac | |
![]() |
fc1b0c1284 | |
![]() |
284bbd6272 | |
![]() |
c3796b4fe8 | |
![]() |
7bde24adaa | |
![]() |
66dbb29871 | |
![]() |
6d0429c21a | |
![]() |
2f0322c1fa | |
![]() |
6093ef131a | |
![]() |
ff89031123 | |
![]() |
d5e7d704bf | |
![]() |
22f7c39dff | |
![]() |
a047d18c76 | |
![]() |
895fee1bae | |
![]() |
3e979c4005 | |
![]() |
f12d5b0525 | |
![]() |
ef7b3faeca | |
![]() |
d4e6393b9e | |
![]() |
663bf4f08d | |
![]() |
06d82e1133 | |
![]() |
f8f98ec41e | |
![]() |
5d2fe252ec | |
![]() |
a8960d67ed | |
![]() |
ebd3f75804 | |
![]() |
c69b4478f7 | |
![]() |
5949c8fb8b | |
![]() |
6a33cfa15a | |
![]() |
c03e92a354 | |
![]() |
b9d10f4670 | |
![]() |
e1e107078e | |
![]() |
3671264984 | |
![]() |
a52cb1fe54 | |
![]() |
bf6bf2e8a8 | |
![]() |
b9c7c0828a | |
![]() |
4f78b4f006 | |
![]() |
9e5ad41e5c | |
![]() |
ca12dc2c29 | |
![]() |
0c1f252641 | |
![]() |
6cc6635988 | |
![]() |
0f6339c0f8 | |
![]() |
2deea6184e | |
![]() |
cf3837094e | |
![]() |
d19b815275 | |
![]() |
5be25c656f | |
![]() |
0f735c62cc | |
![]() |
d68e238b00 | |
![]() |
b6882cd1e5 | |
![]() |
bfb753bf21 | |
![]() |
113c6566d9 | |
![]() |
e39eb316de | |
![]() |
7eb7495537 | |
![]() |
75de7c5f2a | |
![]() |
5677fb180f | |
![]() |
5b042a77e2 | |
![]() |
88a67e4e85 | |
![]() |
0420dc30a4 | |
![]() |
a0706fd52f | |
![]() |
ffe7e119e0 | |
![]() |
1fe5952566 | |
![]() |
7aeb85e4f9 | |
![]() |
df8a7f820d | |
![]() |
fe52ded606 | |
![]() |
8c98f0796b | |
![]() |
e3e67b8dbc | |
![]() |
00da77d785 | |
![]() |
d888588a07 | |
![]() |
be1ea95b71 | |
![]() |
77e25c9056 | |
![]() |
dd3beff508 | |
![]() |
0ddd46643b | |
![]() |
e9950602c4 | |
![]() |
6abff354ee | |
![]() |
35fdc7af14 | |
![]() |
b5b2dd07ee | |
![]() |
4a71f897a8 | |
![]() |
6a1be38613 | |
![]() |
c356679813 |
|
@ -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
|
|
@ -1,11 +1,12 @@
|
|||
src/graphql/generated.ts
|
||||
src/graphql/icuParams.ts
|
||||
src/shared
|
||||
.eslintrc.js
|
||||
graphql-codegen.config.js
|
||||
next-env.d.ts
|
||||
next-sitemap.config.js
|
||||
next.config.js
|
||||
postcss.config.js
|
||||
tailwind.config.js
|
||||
design.config.js
|
||||
graphql.config.js
|
||||
prettier.config.js
|
|
@ -46,7 +46,7 @@ module.exports = {
|
|||
"func-style": ["warn", "expression"],
|
||||
"grouped-accessor-pairs": "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-match": "warn",
|
||||
"max-classes-per-file": ["error", 1],
|
||||
|
@ -161,7 +161,6 @@ module.exports = {
|
|||
"@typescript-eslint/no-invalid-void-type": "error",
|
||||
"@typescript-eslint/no-meaningless-void-operator": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error",
|
||||
"@typescript-eslint/no-parameter-properties": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
// "@typescript-eslint/no-type-alias": "warn",
|
||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
|
||||
|
@ -182,7 +181,6 @@ module.exports = {
|
|||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/sort-type-union-intersection-members": "warn",
|
||||
// "@typescript-eslint/strict-boolean-expressions": [
|
||||
// "error",
|
||||
// { allowAny: true },
|
||||
|
@ -192,7 +190,6 @@ module.exports = {
|
|||
"@typescript-eslint/unified-signatures": "error",
|
||||
|
||||
/* EXTENSION OF ESLINT */
|
||||
"@typescript-eslint/no-duplicate-imports": "error",
|
||||
"@typescript-eslint/default-param-last": "warn",
|
||||
"@typescript-eslint/dot-notation": "warn",
|
||||
"@typescript-eslint/init-declarations": "warn",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Generated content
|
||||
src/graphql/generated.ts
|
||||
|
||||
public/robots.txt
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
|
|
@ -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
|
||||
public/local-data/*
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"editor.rulers": [100],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
145
README.md
145
README.md
|
@ -4,69 +4,127 @@
|
|||
[](https://github.com/Accords-Library/accords-library.com/blob/main/LICENSE)
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
Accord’s Library is a fan-site that aims at gathering and archiving all of Yoko Taro’s work.
|
||||
Yoko Taro is a Japanese video game director and scenario writer. He is best-known for his work on the NieR and Drakengard (Drag-on Dragoon) franchises.
|
||||
|
||||
## Technologies
|
||||
|
||||
#### [Back](https://github.com/Accords-Library/strapi.accords-library.com)
|
||||
### Overview
|
||||
|
||||
- CMS: Stapi
|
||||
- GraphQL endpoint
|
||||
- Multilanguage support
|
||||
- Markdown format for the rich text fields
|
||||
- Use webhooks to notify the front-end and image processor of updates
|
||||

|
||||
|
||||
#### [Image Processor](https://github.com/Accords-Library/img.accords-library.com)
|
||||
_Purple connections are actions done at build-time only. Grey connections can be at build-time or run-time._
|
||||
|
||||
- Convert the images from the CMS to 4 formats
|
||||
- Small: 512x512, quality 60, .webp
|
||||
- Medium: 1024x1024, quality 75, .webp
|
||||
- Large: 2048x2048, quality 80, .webp
|
||||
- Og: 512x512, quality 60, .jpg
|
||||
### [strapi.accords-library.com](https://github.com/Accords-Library/strapi.accords-library.com)
|
||||
|
||||
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
|
||||
Our Content Management System (CMS) that uses [Strapi](https://strapi.io/).
|
||||
|
||||
- Use the official [GraphQL plugin](https://market.strapi.io/plugins/@strapi-plugin-graphql)
|
||||
- Multilanguage support
|
||||
- Markdown format for the rich text fields
|
||||
- Use webhooks to notify the front-end, search engine, and image processor when new content/media has been created/modified/deleted
|
||||
|
||||
### [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.
|
||||
|
||||
Each image in Strapi's media library is converted to four different formats:
|
||||
|
||||
- Small: 512x512, quality 60, .webp
|
||||
- Medium: 1024x1024, quality 75, .webp
|
||||
- Large: 2048x2048, quality 80, .webp
|
||||
- Og: 512x512, quality 60, .jpg
|
||||
|
||||
### [search.accords-library.com](https://github.com/Accords-Library/search.accords-library.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.
|
||||
|
||||
### [gallery.accords-library.com](https://github.com/Accords-Library/gallery.accords-library.com)
|
||||
|
||||
An image board engine, uses [Szurubooru](https://github.com/rr-/szurubooru), a lighweight engine inspired by Danbooru (and Booru-type galleries in general). Unlike the other subdomains, this repo is completely separated from the rest of the stack.
|
||||
|
||||
### [watch.accords-library.com](https://github.com/Accords-Library/watch.accords-library.com)
|
||||
|
||||
A set of tools to archive videos on multiple platforms. The repo contains a CLI tool to archive YouTube videos. There is also a Python script which import the videos metadata to Strapi using GraphQL mutations. Finally, there's a server to serve the video files and thumbnail.
|
||||
|
||||
### [umami.accords-library.com](https://umami.is/)
|
||||
|
||||
An open-source self-hosted alternative to Google Analytics which doesn't require a cookie notice to be GDPR compliant.
|
||||
|
||||
### [accords-library.com](https://github.com/Accords-Library/accords-library.com) (this repository)
|
||||
|
||||
A detailled look at the technologies used in this repository:
|
||||
|
||||
- Language: [TypeScript](https://www.typescriptlang.org/)
|
||||
- Framework: [Next.js 12](https://nextjs.org/) (React 18)
|
||||
|
||||
- Framework: [Next.js 13](https://nextjs.org/) (React 18)
|
||||
|
||||
- SSG + ISR (Static Site Generation + Incremental Static Regeneration)
|
||||
|
||||
- The website is built before running in production
|
||||
- Performances are great, and it's possible to deploy the app on a CDN
|
||||
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
|
||||
- Some widely used data (e.g: UI localizations) are downloaded separetely into `public/local-data` as some form of request deduping + it make this data hot-swappable without the need to rebuild the entire website.
|
||||
|
||||
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
|
||||
|
||||
- Fetch the GraphQL schema from the GraphQL back-end endpoint
|
||||
- Read the operations and fragments stored as graphql files in the `src/graphql` folder
|
||||
- Automatically generates a typesafe ready to use SDK using [graphql-request](https://www.npmjs.com/package/graphql-request) as the GraphQL client
|
||||
- Markdown: [markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx)
|
||||
- Support for arbitrary React Components and Component Props!
|
||||
- Autogenerated multi-level table of content and anchor links for the different headers
|
||||
|
||||
- Styling: [Tailwind CSS](https://tailwindcss.com/)
|
||||
- Support for [Material Icons](https://fonts.google.com/icons)
|
||||
- Support for creating any arbitrary theming mode by swapping CSS variables
|
||||
|
||||
- Support for creating any arbitrary theme by swapping CSS variables
|
||||
- Support for Container Queries (media queries at the element level)
|
||||
- The website has a three-column layout, which turns into one-column + 2 toggleable side-menus if the screen is too narrow.
|
||||
- Check out our [Design System Showcase](https://accords-library.com/dev/showcase/design-system)
|
||||
|
||||
- State Management: [Jōtai](https://jotai.org/)
|
||||
|
||||
- Jōtai is a small-weighted library for atomic state management
|
||||
- Persistent app state using LocalStorage and SessionStorage
|
||||
|
||||
- Markdown
|
||||
|
||||
- Use [Marked](https://www.npmjs.com/package/marked) to convert markdown to HTML (which is then sanitized using [DOMPurify](https://www.npmjs.com/package/isomorphic-dompurify))
|
||||
- Support for arbitrary React Components and Component Props using [markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx)
|
||||
- Autogenerated multi-level table of content and anchor links for the different headers
|
||||
|
||||
- Accessibility
|
||||
|
||||
- Gestures using [react-swipeable](https://www.npmjs.com/package/react-swipeable)
|
||||
- Keyboard hotkeys using [react-hotkeys-hook](https://www.npmjs.com/package/react-hotkeys-hook)
|
||||
- Support for light and dark mode with a manual switch and system's selected theme by default
|
||||
- Fonts can be swaped to [OpenDyslexic](https://www.npmjs.com/package/@fontsource/opendyslexic)
|
||||
|
||||
- Multilingual
|
||||
- By default, use the browser's language as the main language
|
||||
- Fallback languages are used for content which are not available in the main language
|
||||
- Main and fallback languages can be ordered manually by the user
|
||||
- At the content level, the user can know which language is available
|
||||
- Furthermore, the user can temporary select another language then the one that was automatically selected
|
||||
- SSG + ISR (Static Site Generation + Incremental Static Regeneration)
|
||||
- The website is built before running in production
|
||||
- Performances are great, and possibility 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
|
||||
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every page.
|
||||
|
||||
- Users are given a list of supported languages. The first language in this list is the primary language (the language of the UI), the others are fallback languages. The others are fallback languages.
|
||||
- By default, the list is ordered following the browser's languages (and most spoken languages woldwide for the remaining languages). The list can also be reordered manually.
|
||||
- Contents can be available in any number of languages. By default, the best matching language will be presented to the user. However, the user can also decide to temporary select another language for a specific content, without affecting their list of preferred languages.
|
||||
|
||||
- UI Localizations
|
||||
|
||||
- The translated wordings use [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to include variables, plural, dates...
|
||||
- Use a custom ICU Typescript transformation script to provide type safety when formatting ICU wordings
|
||||
- Fallback to English if the translation is missing.
|
||||
|
||||
- SEO
|
||||
|
||||
- Good defaults for the metadata and OpenGraph properties
|
||||
- Each page can provide a custom thumbnail, title, description to be used
|
||||
- Automatic generation of the sitemap using [next-sitemap](https://www.npmjs.com/package/next-sitemap)
|
||||
- Data quality testing
|
||||
|
||||
- Data Quality Testing
|
||||
|
||||
- Data from the CMS is subject to a battery of tests (about 20 warning types and 40 error types) at build time
|
||||
- Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it
|
||||
- Check for completeness, conformity, and integrity
|
||||
- Code quality and style
|
||||
|
||||
- Code Quality and Style
|
||||
|
||||
- React Strict Mode
|
||||
- [Eslint](https://www.npmjs.com/package/eslint) with [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import), [typescript-eslint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin)
|
||||
|
@ -74,8 +132,12 @@
|
|||
- [ts-unused-exports](https://www.npmjs.com/package/ts-unused-exports) to find unused exported functions/constants...
|
||||
|
||||
- Other
|
||||
|
||||
- Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader)
|
||||
- Support for [Material Symbols](https://fonts.google.com/icons)
|
||||
- Custom lightbox using [react-zoom-pan-pinch](https://www.npmjs.com/package/react-zoom-pan-pinch)
|
||||
- Handle query params type-validation using [Zod](https://zod.dev/)
|
||||
- A secret "Terminal" mode. Can you find it?
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -85,29 +147,14 @@ cd accords-library.com
|
|||
npm install
|
||||
```
|
||||
|
||||
Create a env file:
|
||||
Create a env file based on the example one:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
nano .env.local
|
||||
```
|
||||
|
||||
Enter the followind information:
|
||||
|
||||
```txt
|
||||
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_SEARCH=https://url-to.search-accords-library.com
|
||||
NEXT_PUBLIC_UMAMI_URL=https://url-to.umami-accords-library.com
|
||||
NEXT_PUBLIC_UMAMI_ID=abcdef0123456789
|
||||
```
|
||||
Change the variables
|
||||
|
||||
Run in dev mode:
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const colors = {
|
|||
light: { r: 255, g: 237, b: 216 },
|
||||
mid: { r: 240, g: 209, b: 179 },
|
||||
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 },
|
||||
},
|
||||
dark: {
|
||||
|
@ -12,7 +12,7 @@ const colors = {
|
|||
light: { r: 38, g: 34, b: 30 },
|
||||
mid: { r: 57, g: 45, b: 34 },
|
||||
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 },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
<?xml version="1.0"?>
|
||||
<minder version="1.14.0" parent-etag="2844169042" etag="3777682473">
|
||||
<theme name="dark" label="Dark" index="-1"/>
|
||||
<styles>
|
||||
<style level="0" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="rounded" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="10" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="1" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="2" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="3" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="4" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="5" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="6" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="7" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="8" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="9" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<style level="10" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
</styles>
|
||||
<drawarea x="-736.36765543619742" y="32.05864461263036" scale="1.5"/>
|
||||
<images/>
|
||||
<nodes>
|
||||
<node id="0" posx="648.76816813151015" posy="501.13512166341127" width="181" height="56" side="left" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="667.76816813151015" posy="520.13512166341127" maxwidth="200">
|
||||
<text data="accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
<node id="1" posx="603.1911417643222" posy="198.59891764322904" width="228" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="622.1911417643222" posy="217.59891764322904" maxwidth="200">
|
||||
<text data="strapi.accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
<node id="2" posx="508.77593994140574" posy="358.0493469238279" width="230" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="527.7759399414058" posy="377.0493469238279" maxwidth="200">
|
||||
<text data="watch.accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
<node id="3" posx="959.78491210937489" posy="490.81981404622388" width="235" height="56" side="left" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="978.78491210937489" posy="509.81981404622388" maxwidth="200">
|
||||
<text data="search.accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
<node id="4" posx="300.46666463216098" posy="474.56320190429688" width="213" height="56" side="left" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="319.46666463216098" posy="493.56320190429688" maxwidth="200">
|
||||
<text data="img.accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
<node id="5" posx="753.05198160807242" posy="709.79446411132812" width="236" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="772.05198160807242" posy="728.79446411132812" maxwidth="200">
|
||||
<text data="umami.accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
<node id="6" posx="468.00632731119703" posy="709.85610961914062" width="234" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
|
||||
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
|
||||
<nodename posx="487.00632731119703" posy="728.85610961914062" maxwidth="200">
|
||||
<text data="gallery.accords-library.com"/>
|
||||
</nodename>
|
||||
<nodenote></nodenote>
|
||||
</node>
|
||||
</nodes>
|
||||
<groups/>
|
||||
<connections>
|
||||
<connection from_id="3" to_id="1" drag_x="1038.6269124348951" drag_y="296.62844848632801" color="#813d9c">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>GraphQL queries</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="1" to_id="3" drag_x="982.0184326171875" drag_y="347.55404663085926">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>Webhook</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="2" to_id="1" drag_x="640.25118001302098" drag_y="308.37991333007801" color="#813d9c">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>GraphQL mutations</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="0" to_id="5" drag_x="801.02655029296898" drag_y="644.59735107421943">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>Sends events</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="4" to_id="0" drag_x="531.34985351562477" drag_y="582.42803955078148">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="150"/>
|
||||
<title>Provides the images</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="3" to_id="0" drag_x="917.41172281901027" drag_y="585.41107177734443">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="150"/>
|
||||
<title>Provides search results</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="0" to_id="1" drag_x="872.998291015625" drag_y="408.14896647135413">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>GraphQL queries</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="0" to_id="6" drag_x="664.23213704427053" drag_y="645.67006429036542">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>Links to</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="4" to_id="1" drag_x="400.83854166666663" drag_y="295.4715677897135" color="#813d9c">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>Python script</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="2" to_id="0" drag_x="645.96777343749955" drag_y="448.91068522135413">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="150"/>
|
||||
<title>Provides the videos</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="1" to_id="4" drag_x="441.14660644531227" drag_y="342.38249715169252">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>Webhook</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
<connection from_id="1" to_id="0" drag_x="790.7373046875" drag_y="369.4803466796875">
|
||||
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
|
||||
<title>Webhook</title>
|
||||
<note></note>
|
||||
</connection>
|
||||
</connections>
|
||||
<stickers/>
|
||||
</minder>
|
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
|
@ -1,12 +1,11 @@
|
|||
/* CONFIG */
|
||||
|
||||
const locales = ["en", "es", "fr", "pt-br", "ja"];
|
||||
const locales = ["en", "es", "fr", "pt-br", "ja", "zh"];
|
||||
|
||||
/* END CONFIG */
|
||||
|
||||
/* @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
swcMinify: true,
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
i18n: {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
114
package.json
114
package.json
|
@ -2,11 +2,14 @@
|
|||
"name": "accords-library.com",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"dev": "next dev -p 12499",
|
||||
"precommit": "npm run fetch-local-data && npm run prettier && npm run unused-exports && 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",
|
||||
"fetch-local-data": "npm run generate && esrun src/graphql/fetchLocalData.ts",
|
||||
"prebuild": "npm run fetch-local-data",
|
||||
"precommit": "npm run fetch-local-data && npm run icu-to-ts && npm run unused-wording-keys && npm run unused-exports && npm run prettier && npm run eslint && npm run tsc && echo ALL PRECOMMIT CHECKS PASSED SUCCESSFULLY, LET\\'S FUCKING GO!",
|
||||
"unused-wording-keys": "esrun --send-code-mode=temporaryFile src/graphql/unusedWordingKeys.ts --uwk",
|
||||
"unused-exports": "ts-unused-exports ./tsconfig.json --excludePathsFromReport='src/pages;tailwind.config.ts;src/graphql/generated.ts;src/shared/meilisearch-graphql-typings/generated.ts'",
|
||||
"fetch-local-data": "npm run generate && esrun --send-code-mode=temporaryFile src/graphql/fetchLocalData.ts --esrun",
|
||||
"icu-to-ts": "esrun --send-code-mode=temporaryFile src/graphql/icuToTypescript.ts --icu",
|
||||
"prebuild": "npm run fetch-local-data && npm run icu-to-ts",
|
||||
"build": "next build",
|
||||
"postbuild": "next-sitemap --config next-sitemap.config.js",
|
||||
"start": "next start -p 12500",
|
||||
|
@ -14,68 +17,75 @@
|
|||
"eslint": "npx eslint .",
|
||||
"generate": "graphql-codegen --config graphql-codegen.config.js",
|
||||
"tsc": "tsc",
|
||||
"prettier": "prettier --end-of-line auto --write ."
|
||||
"prettier": "prettier --list-different --end-of-line auto --write .",
|
||||
"upgrade": "ncu"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@fontsource/material-icons-outlined": "^4.5.4",
|
||||
"@fontsource/opendyslexic": "^4.5.4",
|
||||
"@fontsource/share-tech-mono": "^4.5.9",
|
||||
"@fontsource/vollkorn": "^4.5.12",
|
||||
"@fontsource/zen-maru-gothic": "^4.5.13",
|
||||
"@fontsource/noto-serif-jp": "^5.0.7",
|
||||
"@fontsource/opendyslexic": "^5.0.7",
|
||||
"@fontsource/share-tech-mono": "^5.0.8",
|
||||
"@fontsource/vollkorn": "^5.0.9",
|
||||
"@fontsource/zen-maru-gothic": "^5.0.7",
|
||||
"@formatjs/icu-messageformat-parser": "^2.6.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"cuid": "^2.1.8",
|
||||
"graphql-request": "^5.0.0",
|
||||
"jotai": "^1.9.0",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"next": "^12.3.1",
|
||||
"nodemailer": "^6.8.0",
|
||||
"rc-slider": "^10.0.1",
|
||||
"react": "18.2.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"isomorphic-dompurify": "^1.8.0",
|
||||
"jotai": "^2.3.1",
|
||||
"markdown-to-jsx": "^7.3.2",
|
||||
"marked": "^7.0.3",
|
||||
"material-symbols": "^0.10.4",
|
||||
"meilisearch": "^0.34.1",
|
||||
"next": "^13.4.17",
|
||||
"nodemailer": "^6.9.4",
|
||||
"patch-package": "^8.0.0",
|
||||
"rc-slider": "^10.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-collapsible": "^2.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-zoom-pan-pinch": "^3.1.0",
|
||||
"string-natural-compare": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"turndown": "^7.1.1",
|
||||
"ua-parser-js": "^1.0.32",
|
||||
"usehooks-ts": "^2.9.1"
|
||||
"turndown": "^7.1.2",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"zod": "^3.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "^3.2.14",
|
||||
"@graphql-codegen/cli": "^2.13.8",
|
||||
"@graphql-codegen/typescript": "2.8.0",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.7",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.5",
|
||||
"@types/node": "18.11.7",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/react": "18.0.22",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@digitak/esrun": "3.2.24",
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-graphql-request": "5.0.0",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@types/html-to-text": "^9.0.1",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@types/node": "20.5.0",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/string-natural-compare": "^3.0.2",
|
||||
"@types/throttle-debounce": "^5.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"graphql": "^16.6.0",
|
||||
"next-sitemap": "^3.1.29",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"ts-unused-exports": "^8.0.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"overrides": {
|
||||
"react-zoom-pan-pinch": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
}
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||
"@typescript-eslint/parser": "^6.4.0",
|
||||
"chalk": "^5.3.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-config-next": "13.4.17",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"graphql": "16.8.0",
|
||||
"graphql-request": "6.1.0",
|
||||
"next-sitemap": "^4.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"ts-unused-exports": "^10.0.0",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -1 +1 @@
|
|||
{"currencies":{"data":[{"id":"1","attributes":{"code":"EUR","symbol":"€","rate_to_usd":1.1062771,"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}}]}}
|
||||
{"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}}]}}
|
|
@ -1 +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 (Traditional)","code":"zh-cht","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","code":"zh","localized_name":"中文"}}]}}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,7 @@
|
|||
import { Ico, Icon } from "./Ico";
|
||||
import { Ico } from "./Ico";
|
||||
import { ToolTip } from "./ToolTip";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -17,13 +16,13 @@ interface Props {
|
|||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const AnchorShare = ({ id, className }: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { format } = useFormat();
|
||||
return (
|
||||
<ToolTip content={langui.copy_anchor_link} trigger="mouseenter" className="text-sm">
|
||||
<ToolTip content={langui.anchor_link_copied} trigger="click" className="text-sm">
|
||||
<ToolTip content={format("copy_anchor_link")} trigger="mouseenter" className="text-sm">
|
||||
<ToolTip content={format("anchor_link_copied")} trigger="click" className="text-sm">
|
||||
<Ico
|
||||
icon={Icon.Link}
|
||||
className={cJoin("transition-color cursor-pointer hover:text-dark", className)}
|
||||
icon="link"
|
||||
className={cJoin("cursor-pointer transition-colors hover:text-dark", className)}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${process.env.NEXT_PUBLIC_URL_SELF + window.location.pathname}#${id}`
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import Head from "next/head";
|
||||
import { useMemo } from "react";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { atom } from "jotai";
|
||||
import { useRouter } from "next/router";
|
||||
import { layout } from "../../design.config";
|
||||
import { Ico, Icon } from "./Ico";
|
||||
import { Ico } from "./Ico";
|
||||
import { MainPanel } from "./Panels/MainPanel";
|
||||
import { SafariPopup } from "./Panels/SafariPopup";
|
||||
import { isDefined, isUndefined } from "helpers/others";
|
||||
import { isDefined, isUndefined } from "helpers/asserts";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph";
|
||||
import { Ids } from "types/ids";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -18,6 +20,7 @@ import { useAtomGetter, useAtomPair } from "helpers/atoms";
|
|||
*/
|
||||
|
||||
const SENSIBILITY_SWIPE = 1.1;
|
||||
const isIOSAtom = atom((get) => get(atoms.userAgent.os) === "iOS");
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -30,7 +33,7 @@ export interface AppLayoutRequired {
|
|||
|
||||
interface Props extends AppLayoutRequired {
|
||||
subPanel?: React.ReactNode;
|
||||
subPanelIcon?: Icon;
|
||||
subPanelIcon?: MaterialSymbol;
|
||||
contentPanel?: React.ReactNode;
|
||||
contentPanelScroolbar?: boolean;
|
||||
}
|
||||
|
@ -41,18 +44,20 @@ export const AppLayout = ({
|
|||
subPanel,
|
||||
contentPanel,
|
||||
openGraph,
|
||||
subPanelIcon = Icon.Tune,
|
||||
subPanelIcon = "tune",
|
||||
contentPanelScroolbar = true,
|
||||
}: Props): JSX.Element => {
|
||||
const isMainPanelReduced = useAtomGetter(atoms.layout.mainPanelReduced);
|
||||
const [isSubPanelOpened, setSubPanelOpened] = useAtomPair(atoms.layout.subPanelOpened);
|
||||
const [isMainPanelOpened, setMainPanelOpened] = useAtomPair(atoms.layout.mainPanelOpened);
|
||||
const isMenuGesturesEnabled = useAtomGetter(atoms.layout.menuGesturesEnabled);
|
||||
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
|
||||
const isIOS = useAtomGetter(isIOSAtom);
|
||||
const router = useRouter();
|
||||
|
||||
const { format } = useFormat();
|
||||
|
||||
const handlers = useSwipeable({
|
||||
onSwipedLeft: (SwipeEventData) => {
|
||||
|
@ -77,18 +82,19 @@ export const AppLayout = ({
|
|||
},
|
||||
});
|
||||
|
||||
const turnSubIntoContent = useMemo(
|
||||
() => isDefined(subPanel) && isUndefined(contentPanel),
|
||||
[contentPanel, subPanel]
|
||||
);
|
||||
const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel) && is1ColumnLayout;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...handlers}
|
||||
id={Ids.Body}
|
||||
className={cJoin(
|
||||
"fixed inset-0 m-0 grid touch-pan-y bg-light p-0 [grid-template-areas:'main_sub_content']",
|
||||
cIf(is1ColumnLayout, "grid-rows-[1fr_5rem] [grid-template-areas:'content''navbar']")
|
||||
"fixed inset-0 m-0 grid touch-pan-y bg-light p-0",
|
||||
cIf(
|
||||
is1ColumnLayout,
|
||||
"grid-rows-[1fr_5rem] [grid-template-areas:'content''navbar']",
|
||||
"[grid-template-areas:'main_sub_content']"
|
||||
)
|
||||
)}
|
||||
style={{
|
||||
gridTemplateColumns: is1ColumnLayout
|
||||
|
@ -101,29 +107,69 @@ export const AppLayout = ({
|
|||
<title>{openGraph.title}</title>
|
||||
<meta name="description" content={openGraph.description} />
|
||||
|
||||
<meta name="twitter:site" content="@AccordsLibrary" />
|
||||
<meta name="twitter:title" content={openGraph.title} />
|
||||
<meta name="twitter:description" content={openGraph.description} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={openGraph.thumbnail.image} />
|
||||
|
||||
<meta
|
||||
property="og:type"
|
||||
content={openGraph.video ? "video.movie" : openGraph.audio ? "music.song" : "website"}
|
||||
/>
|
||||
<meta property="og:locale" content={router.locale} />
|
||||
<meta property="og:site_name" content="Accord’s Library" />
|
||||
|
||||
<meta property="og:title" content={openGraph.title} />
|
||||
<meta property="og:description" content={openGraph.description} />
|
||||
|
||||
<meta property="og:image" content={openGraph.thumbnail.image} />
|
||||
<meta property="og:image:secure_url" content={openGraph.thumbnail.image} />
|
||||
<meta property="og:image:width" content={openGraph.thumbnail.width.toString()} />
|
||||
<meta property="og:image:height" content={openGraph.thumbnail.height.toString()} />
|
||||
<meta property="og:image:alt" content={openGraph.thumbnail.alt} />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
|
||||
{openGraph.audio && (
|
||||
<>
|
||||
<meta property="og:audio" content={openGraph.audio} />
|
||||
<meta property="og:audio:type" content="audio/mpeg" />
|
||||
</>
|
||||
)}
|
||||
{openGraph.video && (
|
||||
<>
|
||||
<meta property="og:video" content={openGraph.video} />{" "}
|
||||
<meta property="og:video:type" content="video/mp4" />
|
||||
</>
|
||||
)}
|
||||
</Head>
|
||||
|
||||
{/* Content panel */}
|
||||
<div
|
||||
id={Ids.ContentPanel}
|
||||
className={cJoin(
|
||||
"bg-light [grid-area:content]",
|
||||
cIf(!isIOS, "texture-paper-dots"),
|
||||
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 */}
|
||||
|
||||
<div
|
||||
className={cJoin(
|
||||
`absolute inset-0 transition-filter duration-500
|
||||
`absolute inset-0 z-40 transition-filter duration-500
|
||||
[grid-area:content]`,
|
||||
cIf(
|
||||
(isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout,
|
||||
"z-10 backdrop-blur",
|
||||
cIf(!isPerfModeEnabled, "backdrop-blur"),
|
||||
"pointer-events-none touch-none"
|
||||
)
|
||||
)}>
|
||||
|
@ -143,64 +189,16 @@ export const AppLayout = ({
|
|||
/>
|
||||
</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={langui.select_option_sidebar ?? ""}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
)}
|
||||
</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 */}
|
||||
<div
|
||||
className={cJoin(
|
||||
`z-10 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
|
||||
border-dotted border-black bg-light texture-paper-dots [grid-area:navbar]`,
|
||||
`z-40 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
|
||||
border-dotted border-black bg-light [grid-area:navbar]`,
|
||||
cIf(!isIOS, "texture-paper-dots"),
|
||||
cIf(!is1ColumnLayout, "hidden")
|
||||
)}>
|
||||
<Ico
|
||||
icon={isMainPanelOpened ? Icon.Close : Icon.Menu}
|
||||
icon={isMainPanelOpened ? "close" : "menu"}
|
||||
className="cursor-pointer !text-2xl"
|
||||
onClick={() => {
|
||||
setMainPanelOpened((current) => !current);
|
||||
|
@ -218,7 +216,7 @@ export const AppLayout = ({
|
|||
</p>
|
||||
{isDefined(subPanel) && !turnSubIntoContent && (
|
||||
<Ico
|
||||
icon={isSubPanelOpened ? Icon.Close : subPanelIcon}
|
||||
icon={isSubPanelOpened ? "close" : subPanelIcon}
|
||||
className="cursor-pointer !text-2xl"
|
||||
onClick={() => {
|
||||
setSubPanelOpened((current) => !current);
|
||||
|
@ -227,16 +225,51 @@ export const AppLayout = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<SafariPopup />
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface ContentPlaceholderProps {
|
||||
message: string;
|
||||
icon?: Icon;
|
||||
icon?: MaterialSymbol;
|
||||
}
|
||||
|
||||
const ContentPlaceholder = ({ message, icon }: ContentPlaceholderProps): JSX.Element => (
|
||||
|
@ -245,7 +278,7 @@ const ContentPlaceholder = ({ message, icon }: ContentPlaceholderProps): JSX.Ele
|
|||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
|
||||
border-dark p-8 text-dark opacity-40">
|
||||
{isDefined(icon) && <Ico icon={icon} className="!text-[300%]" />}
|
||||
<p className={cJoin("w-64 text-2xl", cIf(!isDefined(icon), "text-center"))}>{message}</p>
|
||||
<p className={cJoin("w-64 text-2xl", cIf(isUndefined(icon), "text-center"))}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,8 +16,7 @@ export const Chip = ({ className, text }: Props): JSX.Element => (
|
|||
<div
|
||||
className={cJoin(
|
||||
`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]
|
||||
hover:opacity-100`,
|
||||
border-black/70 px-1.5 pb-[0.14rem] text-xs text-black/70`,
|
||||
className
|
||||
)}>
|
||||
{text}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback } from "react";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { DatePickerFragment } from "graphql/generated";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { DownPressable } from "components/Containers/DownPressable";
|
||||
import { isDefined } from "helpers/others";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
|
@ -17,12 +17,23 @@ interface Props {
|
|||
url: string;
|
||||
active?: 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
|
||||
className="flex w-full gap-4 py-4 px-5"
|
||||
className="flex w-full gap-4 px-5 py-4"
|
||||
href={url}
|
||||
onClick={onClick}
|
||||
active={active}
|
||||
border
|
||||
disabled={disabled}>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { useCallback } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import Collapsible from "react-collapsible";
|
||||
import { TranslatedChroniclePreview } from "./ChroniclePreview";
|
||||
import { GetChroniclesChaptersQuery } from "graphql/generated";
|
||||
import { filterHasAttributes } from "helpers/others";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { prettyInlineTitle, prettySlug, sJoin } from "helpers/formatters";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { compareDate } from "helpers/date";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
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"];
|
||||
currentSlug?: 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 (
|
||||
<div>
|
||||
<div className="grid place-content-center">
|
||||
<div className="grid cursor-pointer grid-cols-[1em_1fr] gap-4" onClick={toggleOpen}>
|
||||
<Ico className="!text-xl" icon={isOpen ? Icon.ArrowDropUp : Icon.ArrowDropDown} />
|
||||
<p className="mb-4 font-headers text-xl">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="grid gap-4 overflow-hidden transition-height duration-500"
|
||||
style={{ maxHeight: isOpen ? `${8 * chronicles.length}rem` : 0 }}>
|
||||
{filterHasAttributes(chronicles, [
|
||||
"attributes.contents",
|
||||
"attributes.translations",
|
||||
] as const)
|
||||
<Collapsible
|
||||
open={open}
|
||||
accordionPosition={title}
|
||||
contentInnerClassName="grid gap-4 pt-4"
|
||||
onTriggerClosing={onTriggerClosing}
|
||||
onOpening={onOpening}
|
||||
easing="ease-in-out"
|
||||
transitionTime={400}
|
||||
lazyRender
|
||||
contentHiddenWhenClosed
|
||||
trigger={
|
||||
<div className="flex place-content-center place-items-center gap-4">
|
||||
<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))
|
||||
.map((chronicle) => (
|
||||
<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
|
||||
? filterHasAttributes(chronicle.attributes.contents.data, [
|
||||
"attributes.translations",
|
||||
] as const).map((content, index) => (
|
||||
]).map((content, index) => (
|
||||
<TranslatedChroniclePreview
|
||||
key={index}
|
||||
active={chronicle.attributes.slug === currentSlug}
|
||||
date={chronicle.attributes.date_start}
|
||||
translations={filterHasAttributes(content.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
]).map((translation) => ({
|
||||
title: prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
|
@ -75,6 +91,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
|||
"/#chronicle-",
|
||||
chronicle.attributes.slug
|
||||
)}
|
||||
onClick={() => setSubPanelOpened(false)}
|
||||
/>
|
||||
))
|
||||
: chronicle.attributes.translations.length > 0 && (
|
||||
|
@ -84,7 +101,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
|||
translations={filterHasAttributes(chronicle.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
"title",
|
||||
] as const).map((translation) => ({
|
||||
]).map((translation) => ({
|
||||
title: translation.title,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
|
@ -101,7 +118,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</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 { useRouter } from "next/router";
|
||||
import { atom } from "jotai";
|
||||
import { cJoin, cIf } from "helpers/className";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
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 previousLinesAtom = atomPairing(atom<string[]>([]));
|
||||
const previousCommandsAtom = atomPairing(atom<string[]>([]));
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
|
@ -33,8 +37,8 @@ export const Terminal = ({
|
|||
const [childrenPaths, setChildrenPaths] = useState(propsChildrenPaths);
|
||||
const setPlayerName = useAtomSetter(atoms.settings.playerName);
|
||||
|
||||
const [previousCommands, setPreviousCommands] = useAtomPair(atoms.terminal.previousCommands);
|
||||
const [previousLines, setPreviousLines] = useAtomPair(atoms.terminal.previousLines);
|
||||
const [previousCommands, setPreviousCommands] = useAtomPair(previousCommandsAtom);
|
||||
const [previousLines, setPreviousLines] = useAtomPair(previousLinesAtom);
|
||||
|
||||
const [line, setLine] = useState("");
|
||||
const [displayCurrentLine, setDisplayCurrentLine] = useState(true);
|
||||
|
@ -112,7 +116,6 @@ export const Terminal = ({
|
|||
key: "rm",
|
||||
description: "Remove files or directories",
|
||||
handle: (currentLine, parameters) => {
|
||||
console.log(parameters);
|
||||
if (parameters.startsWith("-r ")) {
|
||||
const folder = parameters.slice("-r ".length);
|
||||
if (childrenPaths.includes(folder)) {
|
||||
|
@ -246,7 +249,7 @@ export const Terminal = ({
|
|||
|
||||
<div className="relative">
|
||||
<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}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import { cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const ColoredSvg = ({ src, className }: Props): JSX.Element => (
|
||||
<div
|
||||
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
|
||||
)}
|
||||
style={{ mask: `url('${src}')`, WebkitMask: `url('${src}')` }}
|
||||
|
|
|
@ -19,6 +19,12 @@ export enum ContentPanelWidthSizes {
|
|||
Full = "full",
|
||||
}
|
||||
|
||||
const contentPanelWidthSizesToClassName: Record<ContentPanelWidthSizes, string> = {
|
||||
default: "max-w-2xl",
|
||||
large: "max-w-4xl",
|
||||
full: "w-full",
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const ContentPanel = ({
|
||||
|
@ -31,13 +37,9 @@ export const ContentPanel = ({
|
|||
<div className="grid h-full">
|
||||
<main
|
||||
className={cJoin(
|
||||
"relative justify-self-center px-4 pt-10 pb-20",
|
||||
cIf(isContentPanelAtLeast3xl, "px-10 pt-20 pb-32"),
|
||||
width === ContentPanelWidthSizes.Default
|
||||
? "max-w-2xl"
|
||||
: width === ContentPanelWidthSizes.Large
|
||||
? "max-w-4xl"
|
||||
: "w-full",
|
||||
"relative justify-self-center",
|
||||
cIf(isContentPanelAtLeast3xl, "px-10 pb-32 pt-20", "px-4 pb-20 pt-10"),
|
||||
contentPanelWidthSizesToClassName[width],
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface Props {
|
|||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onFocusChanged?: (isFocused: boolean) => void;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Ico } from "components/Ico";
|
||||
import { PageSelector } from "components/Inputs/PageSelector";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { isUndefined } from "helpers/asserts";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollOnChange";
|
||||
import { Ids } from "types/ids";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
page: number;
|
||||
onPageChange: (newPage: number) => void;
|
||||
totalNumberOfPages: number | null | undefined;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Paginator = ({
|
||||
page,
|
||||
onPageChange,
|
||||
totalNumberOfPages,
|
||||
children,
|
||||
}: Props): JSX.Element => {
|
||||
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 (isUndefined(totalNumberOfPages) || totalNumberOfPages < 2) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSelector
|
||||
page={page}
|
||||
onChange={onPageChange}
|
||||
pagesCount={totalNumberOfPages}
|
||||
className="mb-12"
|
||||
/>
|
||||
{children}
|
||||
<PageSelector
|
||||
page={page}
|
||||
onChange={onPageChange}
|
||||
pagesCount={totalNumberOfPages}
|
||||
className="mt-12"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
const DefaultRenderWhenEmpty = () => {
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
const { format } = useFormat();
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
|
@ -1,8 +1,9 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -10,54 +11,90 @@ import { useAtomSetter } from "helpers/atoms";
|
|||
*/
|
||||
|
||||
interface Props {
|
||||
onOpen?: () => void;
|
||||
onCloseRequest?: () => void;
|
||||
isVisible: boolean;
|
||||
children: React.ReactNode;
|
||||
fillViewport?: boolean;
|
||||
hideBackground?: boolean;
|
||||
padding?: boolean;
|
||||
withCloseButton?: boolean;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Popup = ({
|
||||
onOpen,
|
||||
onCloseRequest,
|
||||
isVisible,
|
||||
children,
|
||||
fillViewport,
|
||||
hideBackground = false,
|
||||
padding = true,
|
||||
withCloseButton = true,
|
||||
}: Props): JSX.Element => {
|
||||
const setMenuGesturesEnabled = useAtomSetter(atoms.layout.menuGesturesEnabled);
|
||||
const [isHidden, setHidden] = useState(!isVisible);
|
||||
const [isActuallyVisible, setActuallyVisible] = useState(isVisible && !isHidden);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
|
||||
useHotkeys("escape", () => onCloseRequest?.(), {}, [onCloseRequest]);
|
||||
useHotkeys("escape", () => onCloseRequest?.(), { enabled: isVisible }, [onCloseRequest]);
|
||||
|
||||
useEffect(() => {
|
||||
setMenuGesturesEnabled(!isVisible);
|
||||
}, [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
|
||||
className={cJoin(
|
||||
"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
|
||||
className={cJoin(
|
||||
"fixed inset-0 bg-shade transition-all duration-500",
|
||||
cIf(isVisible, "bg-opacity-50", "bg-opacity-0")
|
||||
"fixed inset-0 transition-colors duration-500",
|
||||
cIf(isActuallyVisible, "bg-shade/50", "bg-shade/0")
|
||||
)}
|
||||
onClick={onCloseRequest}
|
||||
/>
|
||||
|
||||
<div
|
||||
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(isVisible, "scale-100", "scale-0"),
|
||||
cIf(fillViewport, "absolute inset-10", "relative max-h-[80vh] overflow-y-auto"),
|
||||
cIf(!hideBackground, "rounded-lg bg-light shadow-2xl shadow-shade")
|
||||
cIf(isActuallyVisible, "scale-100", "scale-0"),
|
||||
cIf(
|
||||
fillViewport,
|
||||
"absolute inset-10 content-start overflow-scroll",
|
||||
"relative max-h-[80vh] overflow-y-auto"
|
||||
)
|
||||
)}>
|
||||
{withCloseButton && (
|
||||
<div className="absolute right-6 top-6">
|
||||
<Button icon="close" onClick={onCloseRequest} />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@ export const SubPanel = ({ children }: Props): JSX.Element => {
|
|||
<div
|
||||
className={cJoin(
|
||||
"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}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { MouseEventHandler, useState } from "react";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
|
@ -8,29 +15,38 @@ interface Props {
|
|||
className?: string;
|
||||
noBackground?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const UpPressable = ({
|
||||
children,
|
||||
href,
|
||||
className,
|
||||
disabled = false,
|
||||
noBackground = false,
|
||||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
onFocusChanged={setFocused}
|
||||
onClick={onClick}
|
||||
className={cJoin(
|
||||
`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(
|
||||
disabled,
|
||||
"cursor-not-allowed opacity-50 grayscale",
|
||||
cJoin(
|
||||
"cursor-pointer hover:scale-102 hover:drop-shadow-xl",
|
||||
cIf(isFocused, "hover:scale-105 hover:drop-shadow-2xl hover:duration-100")
|
||||
"cursor-pointer hover:scale-102",
|
||||
cIf(isPerfModeEnabled, "hover:shadow-xl", "hover:drop-shadow-xl"),
|
||||
cIf(isFocused, "hover:scale-105 hover:duration-100")
|
||||
)
|
||||
),
|
||||
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 { cIf, cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface PreviewFolderProps {
|
||||
href: string;
|
||||
title?: string | null;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const PreviewFolder = ({ href, title, disabled }: PreviewFolderProps): JSX.Element => (
|
||||
<UpPressable href={href} disabled={disabled}>
|
||||
<div
|
||||
|
@ -23,7 +30,10 @@ export const PreviewFolder = ({ href, title, disabled }: PreviewFolderProps): JS
|
|||
</UpPressable>
|
||||
);
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ TRANSLATED VARIANT ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
export const TranslatedPreviewFolder = ({
|
||||
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>
|
||||
);
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@ import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img";
|
|||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
* ────────────────────────────────────────╯ COMPONENT ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { MouseEventHandler, useCallback } from "react";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { Link } from "./Link";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { Ico } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
|
||||
|
@ -17,15 +17,16 @@ interface Props {
|
|||
className?: string;
|
||||
href?: string;
|
||||
active?: boolean;
|
||||
icon?: Icon;
|
||||
icon?: MaterialSymbol;
|
||||
text?: string | null | undefined;
|
||||
alwaysNewTab?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseUp?: MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseUp?: MouseEventHandler<HTMLButtonElement>;
|
||||
draggable?: boolean;
|
||||
badgeNumber?: number;
|
||||
disabled?: boolean;
|
||||
size?: "normal" | "small";
|
||||
type?: "button" | "reset" | "submit";
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
@ -35,7 +36,7 @@ export const Button = ({
|
|||
id,
|
||||
onClick,
|
||||
onMouseUp,
|
||||
active,
|
||||
active = false,
|
||||
className,
|
||||
icon,
|
||||
text,
|
||||
|
@ -43,54 +44,59 @@ export const Button = ({
|
|||
alwaysNewTab = false,
|
||||
badgeNumber,
|
||||
disabled,
|
||||
type,
|
||||
size = "normal",
|
||||
}: Props): JSX.Element => (
|
||||
<ConditionalWrapper
|
||||
isWrapping={isDefinedAndNotEmpty(href) && !disabled}
|
||||
wrapperProps={{ href: href ?? "", alwaysNewTab }}
|
||||
wrapper={LinkWrapper}>
|
||||
<Link href={href} alwaysNewTab={alwaysNewTab} disabled={disabled}>
|
||||
<div className="relative">
|
||||
<div
|
||||
<button
|
||||
type={type}
|
||||
draggable={draggable}
|
||||
id={id}
|
||||
onClick={(event) => !disabled && onClick?.(event)}
|
||||
disabled={disabled}
|
||||
onClick={(event) => onClick?.(event)}
|
||||
onMouseUp={onMouseUp}
|
||||
onFocus={(event) => event.target.blur()}
|
||||
className={cJoin(
|
||||
`group grid cursor-pointer select-none grid-flow-col place-content-center
|
||||
place-items-center gap-2 rounded-full border border-dark py-3 px-4
|
||||
leading-none text-dark transition-all`,
|
||||
cIf(size === "small", "px-3 py-1 text-xs"),
|
||||
cIf(active, "!border-black bg-black !text-light drop-shadow-lg shadow-black"),
|
||||
`group grid w-full grid-flow-col
|
||||
place-content-center place-items-center gap-2 rounded-full border
|
||||
border-dark leading-none text-dark transition-all
|
||||
disabled:cursor-not-allowed disabled:opacity-50 disabled:grayscale`,
|
||||
cIf(size === "small", "h-6 px-3 py-1 text-xs", "h-10 px-4 py-3"),
|
||||
cIf(active, "!border-black bg-black !text-light shadow-lg shadow-black"),
|
||||
cIf(
|
||||
disabled,
|
||||
"cursor-not-allowed opacity-50 grayscale",
|
||||
cIf(
|
||||
!active,
|
||||
`shadow-shade hover:bg-dark hover:text-light hover:drop-shadow-lg
|
||||
active:hover:!border-black active:hover:bg-black active:hover:!text-light
|
||||
active:hover:drop-shadow-lg active:hover:shadow-black`
|
||||
)
|
||||
!disabled && !active,
|
||||
`shadow-shade hover:bg-dark hover:text-light hover:shadow-lg hover:shadow-shade
|
||||
active:hover:!border-black active:hover:bg-black active:hover:!text-light
|
||||
active:hover:shadow-lg active:hover:shadow-black`
|
||||
),
|
||||
className
|
||||
)}>
|
||||
{isDefined(badgeNumber) && (
|
||||
<div
|
||||
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`,
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
{isDefinedAndNotEmpty(icon) && (
|
||||
<Ico className="[font-size:150%] [line-height:0.66]" icon={icon} />
|
||||
<Ico
|
||||
className="![font-size:150%] ![line-height:0.66]"
|
||||
icon={icon}
|
||||
isFilled={active}
|
||||
opticalSize={size === "normal" ? 24 : 20}
|
||||
weight={size === "normal" ? 500 : 800}
|
||||
/>
|
||||
)}
|
||||
{isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>}
|
||||
</div>
|
||||
{isDefinedAndNotEmpty(text) && (
|
||||
<p className="line-clamp-1 -translate-y-[0.05em] text-center leading-5">{text}</p>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</ConditionalWrapper>
|
||||
</Link>
|
||||
);
|
||||
|
||||
/*
|
||||
|
@ -110,19 +116,3 @@ export const TranslatedButton = ({
|
|||
|
||||
return <Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} />;
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface LinkWrapperProps {
|
||||
href: string;
|
||||
alwaysNewTab: boolean;
|
||||
}
|
||||
|
||||
const LinkWrapper = ({ children, alwaysNewTab, href }: LinkWrapperProps & Wrapper) => (
|
||||
<Link href={href} alwaysNewTab={alwaysNewTab}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -1,57 +1,110 @@
|
|||
import type { Placement } from "tippy.js";
|
||||
import { Button } from "./Button";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
type ButtonProps = Parameters<typeof Button>[0];
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
className?: string;
|
||||
buttonsProps: (Parameters<typeof Button>[0] & {
|
||||
tooltip?: string | null | undefined;
|
||||
vertical?: boolean;
|
||||
size?: ButtonProps["size"];
|
||||
buttonsProps: (Omit<ButtonProps, "size"> & {
|
||||
visible?: boolean;
|
||||
tooltip?: React.ReactNode | null | undefined;
|
||||
tooltipPlacement?: Placement;
|
||||
})[];
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const ButtonGroup = ({ buttonsProps, className }: Props): JSX.Element => (
|
||||
<div className={cJoin("grid grid-flow-col", className)}>
|
||||
{buttonsProps.map((buttonProps, index) => (
|
||||
<ConditionalWrapper
|
||||
key={index}
|
||||
isWrapping={isDefinedAndNotEmpty(buttonProps.tooltip)}
|
||||
wrapper={ToolTipWrapper}
|
||||
wrapperProps={{ text: buttonProps.tooltip ?? "" }}>
|
||||
<Button
|
||||
{...buttonProps}
|
||||
className={
|
||||
index === 0
|
||||
? "rounded-r-none border-r-0"
|
||||
: index === buttonsProps.length - 1
|
||||
? "rounded-l-none"
|
||||
: "rounded-none border-r-0"
|
||||
}
|
||||
/>
|
||||
</ConditionalWrapper>
|
||||
))}
|
||||
</div>
|
||||
export const ButtonGroup = ({
|
||||
buttonsProps,
|
||||
className,
|
||||
vertical,
|
||||
size,
|
||||
}: ButtonGroupProps): JSX.Element => (
|
||||
<FilteredButtonGroup
|
||||
buttonsProps={buttonsProps.filter((button) => button.visible !== false)}
|
||||
className={className}
|
||||
vertical={vertical}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
|
||||
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 ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface ToolTipWrapperProps {
|
||||
text: string;
|
||||
text: React.ReactNode;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
const ToolTipWrapper = ({ text, children }: ToolTipWrapperProps & Wrapper) => (
|
||||
<ToolTip content={text}>
|
||||
const ToolTipWrapper = ({ text, children, placement }: ToolTipWrapperProps & Wrapper) => (
|
||||
<ToolTip content={text} placement={placement}>
|
||||
<>{children}</>
|
||||
</ToolTip>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { Fragment } from "react";
|
||||
import { ToolTip } from "../ToolTip";
|
||||
import { Button } from "./Button";
|
||||
import { Icon } from "components/Ico";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { prettyLanguage } from "helpers/formatters";
|
||||
import { iterateMap } from "helpers/others";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -33,7 +30,7 @@ export const LanguageSwitcher = ({
|
|||
onLanguageChanged,
|
||||
showBadge = true,
|
||||
}: Props): JSX.Element => {
|
||||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
const { formatLanguage } = useFormat();
|
||||
return (
|
||||
<ToolTip
|
||||
content={
|
||||
|
@ -46,7 +43,7 @@ export const LanguageSwitcher = ({
|
|||
onLanguageChanged(value);
|
||||
sendAnalytics("Language Switcher", `Switch language (${locale})`);
|
||||
}}
|
||||
text={prettyLanguage(locale, languages)}
|
||||
text={formatLanguage(locale)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
@ -54,7 +51,7 @@ export const LanguageSwitcher = ({
|
|||
}>
|
||||
<Button
|
||||
badgeNumber={showBadge && locales.size > 1 ? locales.size : undefined}
|
||||
icon={Icon.Translate}
|
||||
icon="translate"
|
||||
size={size}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
|
|
@ -1,74 +1,100 @@
|
|||
import router from "next/router";
|
||||
import { MouseEventHandler, useState } from "react";
|
||||
import { isDefined } from "helpers/others";
|
||||
import React, { MouseEventHandler } from "react";
|
||||
import NextLink from "next/link";
|
||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
href: string | null | undefined;
|
||||
className?: string;
|
||||
allowNewTab?: boolean;
|
||||
alwaysNewTab?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
onFocusChanged?: (isFocused: boolean) => void;
|
||||
disabled?: boolean;
|
||||
linkStyled?: boolean;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Link = ({
|
||||
href,
|
||||
allowNewTab = true,
|
||||
alwaysNewTab = false,
|
||||
disabled = false,
|
||||
children,
|
||||
className,
|
||||
alwaysNewTab,
|
||||
disabled,
|
||||
linkStyled = false,
|
||||
onClick,
|
||||
onFocusChanged,
|
||||
}: Props): JSX.Element => {
|
||||
const [isValidClick, setIsValidClick] = useState(false);
|
||||
}: Props): JSX.Element => (
|
||||
<ConditionalWrapper
|
||||
isWrapping={isDefinedAndNotEmpty(href) && !disabled}
|
||||
wrapperProps={{
|
||||
href: href ?? "",
|
||||
alwaysNewTab,
|
||||
onClick,
|
||||
onFocusChanged,
|
||||
className: cJoin(
|
||||
cIf(
|
||||
linkStyled,
|
||||
`underline decoration-dark decoration-dotted underline-offset-2 transition-colors
|
||||
hover:text-dark`
|
||||
),
|
||||
className
|
||||
),
|
||||
}}
|
||||
wrapper={LinkWrapper}
|
||||
wrapperFalse={DisabledWrapper}
|
||||
wrapperFalseProps={{ className }}>
|
||||
{children}
|
||||
</ConditionalWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onMouseLeave={() => {
|
||||
setIsValidClick(false);
|
||||
onFocusChanged?.(false);
|
||||
}}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
onMouseDown={(event) => {
|
||||
if (!disabled) {
|
||||
event.preventDefault();
|
||||
onFocusChanged?.(true);
|
||||
setIsValidClick(true);
|
||||
}
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
onFocusChanged?.(false);
|
||||
if (!disabled) {
|
||||
if (isDefined(onClick)) {
|
||||
onClick(event);
|
||||
} else if (isValidClick && href) {
|
||||
if (event.button !== MouseButton.Right) {
|
||||
if (alwaysNewTab) {
|
||||
window.open(href, "_blank", "noopener");
|
||||
} else if (event.button === MouseButton.Left) {
|
||||
if (href.startsWith("#")) {
|
||||
router.replace(href);
|
||||
} else {
|
||||
router.push(href);
|
||||
}
|
||||
} else if (allowNewTab) {
|
||||
window.open(href, "_blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
enum MouseButton {
|
||||
Left = 0,
|
||||
Middle = 1,
|
||||
Right = 2,
|
||||
interface LinkWrapperProps {
|
||||
href: string;
|
||||
className?: string;
|
||||
alwaysNewTab?: boolean;
|
||||
onFocusChanged?: (isFocused: boolean) => void;
|
||||
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
const LinkWrapper = ({
|
||||
children,
|
||||
className,
|
||||
onFocusChanged,
|
||||
onClick,
|
||||
alwaysNewTab = false,
|
||||
href,
|
||||
}: LinkWrapperProps & Wrapper) => (
|
||||
<NextLink
|
||||
href={href}
|
||||
className={className}
|
||||
target={alwaysNewTab ? "_blank" : "_self"}
|
||||
replace={href.startsWith("#")}
|
||||
onClick={onClick}
|
||||
onMouseLeave={() => onFocusChanged?.(false)}
|
||||
onMouseDown={() => onFocusChanged?.(true)}
|
||||
onMouseUp={() => onFocusChanged?.(false)}>
|
||||
{children}
|
||||
</NextLink>
|
||||
);
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface DisabledWrapperProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DisabledWrapper = ({ children, className }: DisabledWrapperProps & Wrapper) => (
|
||||
<div className={className}>{children}</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Fragment, useCallback } from "react";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { arrayMove, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { Ico } from "components/Ico";
|
||||
import { arrayMove } from "helpers/others";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -16,10 +17,17 @@ interface Props {
|
|||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface InsertedLabelProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const InsertedLabel = ({ label }: InsertedLabelProps) => (
|
||||
<>{isDefinedAndNotEmpty(label) && <p>{label}</p>}</>
|
||||
);
|
||||
|
||||
export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Element => {
|
||||
const updateOrder = useCallback(
|
||||
(sourceIndex: number, targetIndex: number) => {
|
||||
console.log("updateOrder");
|
||||
onChange?.(arrayMove(items, sourceIndex, targetIndex));
|
||||
},
|
||||
[items, onChange]
|
||||
|
@ -29,9 +37,8 @@ export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Ele
|
|||
<div className="grid gap-2">
|
||||
{items.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
{insertLabels && isDefinedAndNotEmpty(insertLabels[index]?.name) && (
|
||||
<p>{insertLabels[index].name}</p>
|
||||
)}
|
||||
<InsertedLabel label={insertLabels?.[index]?.name} />
|
||||
|
||||
<div
|
||||
onDragStart={(event) => {
|
||||
const source = event.target as HTMLElement;
|
||||
|
@ -63,7 +70,7 @@ export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Ele
|
|||
<div className="grid grid-rows-[.8em_.8em] place-items-center">
|
||||
{index > 0 && (
|
||||
<Ico
|
||||
icon={Icon.ArrowDropUp}
|
||||
icon="arrow_drop_up"
|
||||
className="row-start-1 cursor-pointer"
|
||||
onClick={() => {
|
||||
updateOrder(index, index - 1);
|
||||
|
@ -72,7 +79,7 @@ export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Ele
|
|||
)}
|
||||
{index < items.length - 1 && (
|
||||
<Ico
|
||||
icon={Icon.ArrowDropDown}
|
||||
icon="arrow_drop_down"
|
||||
className="row-start-2 cursor-pointer"
|
||||
onClick={() => {
|
||||
updateOrder(index, index + 1);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ButtonGroup } from "./ButtonGroup";
|
||||
import { Icon } from "components/Ico";
|
||||
import { cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
|
@ -21,25 +20,25 @@ export const PageSelector = ({ page, className, pagesCount, onChange }: Props):
|
|||
className={cJoin("flex flex-row place-content-center", className)}
|
||||
buttonsProps={[
|
||||
{
|
||||
onClick: () => onChange(0),
|
||||
disabled: page === 0,
|
||||
icon: Icon.FirstPage,
|
||||
onClick: () => onChange(1),
|
||||
disabled: page === 1,
|
||||
icon: "first_page",
|
||||
},
|
||||
{
|
||||
onClick: () => page > 0 && onChange(page - 1),
|
||||
disabled: page === 0,
|
||||
icon: Icon.NavigateBefore,
|
||||
onClick: () => page > 1 && onChange(page - 1),
|
||||
disabled: page === 1,
|
||||
icon: "navigate_before",
|
||||
},
|
||||
{ text: `${page + 1} / ${pagesCount}` },
|
||||
{ text: `${page} / ${pagesCount}` },
|
||||
{
|
||||
onClick: () => page < pagesCount - 1 && onChange(page + 1),
|
||||
disabled: page === pagesCount - 1,
|
||||
icon: Icon.NavigateNext,
|
||||
onClick: () => page < pagesCount && onChange(page + 1),
|
||||
disabled: page === pagesCount,
|
||||
icon: "navigate_next",
|
||||
},
|
||||
{
|
||||
onClick: () => onChange(pagesCount - 1),
|
||||
disabled: page === pagesCount - 1,
|
||||
icon: Icon.LastPage,
|
||||
onClick: () => onChange(pagesCount),
|
||||
disabled: page === pagesCount,
|
||||
icon: "last_page",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Fragment, useCallback, useRef } from "react";
|
||||
import { useBoolean, useOnClickOutside } from "usehooks-ts";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { Ico } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
|
@ -52,19 +52,18 @@ export const Select = ({
|
|||
ref={ref}
|
||||
className={cJoin(
|
||||
"relative text-center transition-filter",
|
||||
cIf(isOpened, "z-10 drop-shadow-lg shadow-shade"),
|
||||
cIf(isOpened, "z-20 drop-shadow-lg shadow-shade"),
|
||||
className
|
||||
)}>
|
||||
<div
|
||||
className={cJoin(
|
||||
`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
|
||||
outline-mid`,
|
||||
place-items-center rounded-3xl p-1 outline outline-1 -outline-offset-1`,
|
||||
cIf(isOpened, "rounded-b-none bg-highlight outline-transparent"),
|
||||
cIf(
|
||||
disabled,
|
||||
"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">
|
||||
|
@ -72,12 +71,12 @@ export const Select = ({
|
|||
</p>
|
||||
{value >= 0 && allowEmpty && (
|
||||
<Ico
|
||||
icon={Icon.Close}
|
||||
icon="close"
|
||||
className="!text-xs"
|
||||
onClick={() => !disabled && onSelectionChanged(-1)}
|
||||
/>
|
||||
)}
|
||||
<Ico onClick={tryToggling} icon={isOpened ? Icon.ArrowDropUp : Icon.ArrowDropDown} />
|
||||
<Ico onClick={tryToggling} icon={isOpened ? "arrow_drop_up" : "arrow_drop_down"} />
|
||||
</div>
|
||||
<div className={cJoin("left-0 right-0 rounded-b-[1em]", cIf(isOpened, "absolute", "hidden"))}>
|
||||
{options.map((option, index) => (
|
||||
|
|
|
@ -21,10 +21,14 @@ export const Switch = ({ value, onClick, className, disabled = false }: Props):
|
|||
<div
|
||||
className={cJoin(
|
||||
`relative grid h-6 w-12 content-center rounded-full border-mid outline
|
||||
outline-1 -outline-offset-1 outline-mid transition-colors`,
|
||||
cIf(value, "border-none bg-mid shadow-inner-sm outline-transparent shadow-shade"),
|
||||
cIf(disabled, "cursor-not-allowed opacity-50 grayscale", "cursor-pointer"),
|
||||
cIf(disabled, cIf(value, "bg-dark/40 outline-transparent", "outline-dark/60")),
|
||||
outline-1 -outline-offset-1 transition-colors`,
|
||||
cIf(value, "border-none shadow-inner-sm shadow-shade"),
|
||||
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(value, "bg-mid outline-transparent")
|
||||
),
|
||||
className
|
||||
)}
|
||||
onClick={() => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Ico, Icon } from "components/Ico";
|
||||
import { forwardRef } from "react";
|
||||
import { Ico } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -12,40 +13,38 @@ interface Props {
|
|||
onChange: (newValue: string) => void;
|
||||
className?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
placeholder?: string | null;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const TextInput = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
name,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: Props): JSX.Element => (
|
||||
<div className={cJoin("relative", className)}>
|
||||
<input
|
||||
className="w-full"
|
||||
type="text"
|
||||
name={name}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{isDefinedAndNotEmpty(value) && (
|
||||
<div className="absolute right-4 top-0 bottom-0 grid place-items-center">
|
||||
<Ico
|
||||
className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))}
|
||||
icon={Icon.Close}
|
||||
onClick={() => !disabled && onChange("")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
export const TextInput = forwardRef<HTMLInputElement, Props>(
|
||||
({ value, onChange, className, name, placeholder, disabled = false }, ref) => (
|
||||
<div className={cJoin("relative", className)}>
|
||||
<input
|
||||
ref={ref}
|
||||
className="w-full"
|
||||
type="text"
|
||||
name={name}
|
||||
autoCapitalize="off"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder ?? undefined}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{isDefinedAndNotEmpty(value) && (
|
||||
<div className="absolute bottom-0 right-4 top-0 grid place-items-center">
|
||||
<Ico
|
||||
className={cJoin("!text-xs", cIf(disabled, "opacity-30 grayscale", "cursor-pointer"))}
|
||||
icon="close"
|
||||
onClick={() => !disabled && onChange("")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
TextInput.displayName = "TextInput";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { cIf, cJoin } from "helpers/className";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { Icon } from "components/Ico";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { LibraryItemUserStatus } from "types/types";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -21,7 +19,7 @@ interface Props {
|
|||
|
||||
export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
|
||||
const { libraryItemUserStatus, setLibraryItemUserStatus } = useLibraryItemUserStatus();
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { format } = useFormat();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -29,13 +27,13 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
|
|||
"flex flex-row flex-wrap place-content-center place-items-center",
|
||||
cIf(expand, "gap-4", "gap-2")
|
||||
)}>
|
||||
<ToolTip content={langui.want_it} disabled={expand}>
|
||||
<ToolTip content={format("want_it")} disabled={expand}>
|
||||
<Button
|
||||
icon={Icon.Favorite}
|
||||
text={expand ? langui.want_it : undefined}
|
||||
icon="favorite"
|
||||
text={expand ? format("want_it") : undefined}
|
||||
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Want}
|
||||
onMouseUp={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setLibraryItemUserStatus((current) => {
|
||||
const newLibraryItemUserStatus = { ...current };
|
||||
newLibraryItemUserStatus[id] =
|
||||
|
@ -47,13 +45,13 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
|
|||
}}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.have_it} disabled={expand}>
|
||||
<ToolTip content={format("have_it")} disabled={expand}>
|
||||
<Button
|
||||
icon={Icon.BackHand}
|
||||
text={expand ? langui.have_it : undefined}
|
||||
icon="back_hand"
|
||||
text={expand ? format("have_it") : undefined}
|
||||
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Have}
|
||||
onMouseUp={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setLibraryItemUserStatus((current) => {
|
||||
const newLibraryItemUserStatus = { ...current };
|
||||
newLibraryItemUserStatus[id] =
|
||||
|
|
|
@ -3,16 +3,20 @@ import { useState } from "react";
|
|||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Img } from "./Img";
|
||||
import { Button } from "./Inputs/Button";
|
||||
import { Icon } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { useFullscreen } from "hooks/useFullscreen";
|
||||
import { Ids } from "types/ids";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { isDefined } from "helpers/others";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
onCloseRequest: () => void;
|
||||
isVisible: boolean;
|
||||
|
@ -35,6 +39,7 @@ export const LightBox = ({
|
|||
onPressNext,
|
||||
}: Props): JSX.Element => {
|
||||
const [currentZoom, setCurrentZoom] = useState(1);
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const { isFullscreen, toggleFullscreen, exitFullscreen, requestFullscreen } = useFullscreen(
|
||||
Ids.LightBox
|
||||
);
|
||||
|
@ -58,12 +63,12 @@ export const LightBox = ({
|
|||
id={Ids.LightBox}
|
||||
className={cJoin(
|
||||
"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
|
||||
className={cJoin(
|
||||
"fixed inset-0 bg-shade transition-all duration-500",
|
||||
cIf(isVisible, "bg-opacity-50", "bg-opacity-0")
|
||||
"fixed inset-0 transition-colors duration-500",
|
||||
cIf(isVisible, "bg-shade/50", "bg-shade/0")
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
|
@ -86,8 +91,10 @@ export const LightBox = ({
|
|||
}}>
|
||||
{isDefined(src) && (
|
||||
<Img
|
||||
className={`h-[calc(100vh-4rem)] w-full object-contain drop-shadow-2xl
|
||||
shadow-shade`}
|
||||
className={cJoin(
|
||||
`h-[calc(100vh-4rem)] w-full object-contain`,
|
||||
cIf(!isPerfModeEnabled, "drop-shadow-2xl shadow-shade")
|
||||
)}
|
||||
src={src}
|
||||
quality={ImageQuality.Large}
|
||||
/>
|
||||
|
@ -120,6 +127,11 @@ export const LightBox = ({
|
|||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface ControlButtonsProps {
|
||||
isPreviousImageAvailable: boolean;
|
||||
isNextImageAvailable: boolean;
|
||||
|
@ -142,24 +154,17 @@ const ControlButtons = ({
|
|||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const PreviousButton = () => (
|
||||
<Button
|
||||
icon={Icon.NavigateBefore}
|
||||
onClick={onPressPrevious}
|
||||
disabled={!isPreviousImageAvailable}
|
||||
/>
|
||||
<Button icon="navigate_before" onClick={onPressPrevious} disabled={!isPreviousImageAvailable} />
|
||||
);
|
||||
const NextButton = () => (
|
||||
<Button icon={Icon.NavigateNext} onClick={onPressNext} disabled={!isNextImageAvailable} />
|
||||
<Button icon="navigate_next" onClick={onPressNext} disabled={!isNextImageAvailable} />
|
||||
);
|
||||
|
||||
const FullscreenButton = () => (
|
||||
<Button
|
||||
icon={isFullscreen ? Icon.FullscreenExit : Icon.Fullscreen}
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
<Button icon={isFullscreen ? "fullscreen_exit" : "fullscreen"} onClick={toggleFullscreen} />
|
||||
);
|
||||
|
||||
const CloseButton = () => <Button onClick={onCloseRequest} icon={Icon.Close} />;
|
||||
const CloseButton = () => <Button onClick={onCloseRequest} icon="close" />;
|
||||
|
||||
return is1ColumnLayout ? (
|
||||
<>
|
||||
|
@ -170,7 +175,7 @@ const ControlButtons = ({
|
|||
<NextButton />
|
||||
</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 />
|
||||
</div>
|
||||
</>
|
||||
|
@ -178,20 +183,20 @@ const ControlButtons = ({
|
|||
<>
|
||||
{isPreviousImageAvailable && (
|
||||
<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`}>
|
||||
<PreviousButton />
|
||||
</div>
|
||||
)}
|
||||
{isNextImageAvailable && (
|
||||
<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`}>
|
||||
<NextButton />
|
||||
</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`}>
|
||||
<CloseButton />
|
||||
<FullscreenButton />
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import Markdown from "markdown-to-jsx";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment, useMemo } from "react";
|
||||
import React, { Fragment, MouseEventHandler, useMemo } from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { z } from "zod";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Img } from "components/Img";
|
||||
import { InsetBox } from "components/Containers/InsetBox";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { slugify } from "helpers/formatters";
|
||||
import { getAssetURL, ImageQuality } from "helpers/img";
|
||||
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
|
||||
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
|
||||
import { AnchorShare } from "components/AnchorShare";
|
||||
import { useIntersectionList } from "hooks/useIntersectionList";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { Ico } from "components/Ico";
|
||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { VideoPlayer } from "components/Player";
|
||||
import { getVideoFile } from "helpers/videos";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -30,16 +34,12 @@ interface MarkdawnProps {
|
|||
|
||||
export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Element => {
|
||||
const playerName = useAtomGetter(atoms.settings.playerName);
|
||||
const router = useRouter();
|
||||
const isContentPanelAtLeastLg = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastLg);
|
||||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
const text = useMemo(
|
||||
() => `${preprocessMarkDawn(rawText, playerName)}
|
||||
`,
|
||||
[playerName, rawText]
|
||||
);
|
||||
const text = `${preprocessMarkDawn(rawText, playerName)}
|
||||
`;
|
||||
/* eslint-enable no-irregular-whitespace */
|
||||
|
||||
if (isUndefined(text) || text === "") {
|
||||
|
@ -56,13 +56,15 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
component: (compProps: { href: string; children: React.ReactNode }) => {
|
||||
if (compProps.href.startsWith("/") || compProps.href.startsWith("#")) {
|
||||
return (
|
||||
<a onClick={async () => router.push(compProps.href)}>{compProps.children}</a>
|
||||
<Link href={compProps.href} linkStyled>
|
||||
{compProps.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={compProps.href} target="_blank" rel="noreferrer">
|
||||
<Link href={compProps.href} alwaysNewTab linkStyled>
|
||||
{compProps.children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -98,9 +100,9 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
? slugify(compProps.target)
|
||||
: slugify(compProps.children?.toString());
|
||||
return (
|
||||
<a onClick={async () => router.replace(`${compProps.page ?? ""}#${slug}`)}>
|
||||
<Link href={`${compProps.page ?? ""}#${slug}`} linkStyled>
|
||||
{compProps.children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -118,14 +120,43 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
},
|
||||
|
||||
Line: {
|
||||
component: (compProps) => (
|
||||
<>
|
||||
<strong
|
||||
className={cJoin("!my-0 text-dark/60", cIf(!isContentPanelAtLeastLg, "!-mb-4"))}>
|
||||
<Markdawn text={compProps.name} />
|
||||
</strong>
|
||||
<p className="whitespace-pre-line">{compProps.children}</p>
|
||||
</>
|
||||
component: (compProps) => {
|
||||
const schema = z.object({ name: z.string(), children: z.any() });
|
||||
if (!schema.safeParse(compProps).success) {
|
||||
return (
|
||||
<MarkdawnError
|
||||
message={`Error while parsing a <Line/> tag. Here is the correct usage:
|
||||
<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"
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
|
@ -187,7 +218,7 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
name?: string;
|
||||
}) => (
|
||||
<div
|
||||
className="mt-8 mb-12 grid cursor-pointer place-content-center"
|
||||
className="mb-12 mt-8 grid cursor-pointer place-content-center"
|
||||
onClick={() => {
|
||||
showLightBox([
|
||||
compProps.src.startsWith("/uploads/")
|
||||
|
@ -216,38 +247,47 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface TableOfContentsProps {
|
||||
text: string;
|
||||
title?: string;
|
||||
|
||||
horizontalLine?: boolean;
|
||||
interface MarkdawnErrorProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const TableOfContents = ({
|
||||
text,
|
||||
title,
|
||||
const MarkdawnError = ({ message }: MarkdawnErrorProps): JSX.Element => (
|
||||
<div
|
||||
className="flex place-items-center gap-4 whitespace-pre-line rounded-md
|
||||
bg-[red]/10 px-4 text-[red]">
|
||||
<Ico icon="error" isFilled={false} />
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
horizontalLine = false,
|
||||
}: TableOfContentsProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const toc = useMemo(() => getTocFromMarkdawn(preprocessMarkDawn(text), title), [text, title]);
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface TableOfContentsProps {
|
||||
toc: TocInterface;
|
||||
onContentClicked?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
export const TableOfContents = ({ toc, onContentClicked }: TableOfContentsProps): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
|
||||
return (
|
||||
<>
|
||||
{toc.children.length > 0 && (
|
||||
<>
|
||||
{horizontalLine && <HorizontalLine />}
|
||||
<h3 className="text-xl">{langui.table_of_contents}</h3>
|
||||
<h3 className="text-xl">{format("table_of_contents")}</h3>
|
||||
<div className="max-w-[14.5rem] text-left">
|
||||
<p
|
||||
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
|
||||
text-left">
|
||||
<a onClick={async () => router.replace(`#${toc.slug}`)}>
|
||||
<Link href={`#${toc.slug}`} linkStyled onClick={onContentClicked}>
|
||||
{<abbr title={toc.title}>{toc.title}</abbr>}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<TocLevel tocchildren={toc.children} parentNumbering="" />
|
||||
<TocLevel
|
||||
tocchildren={toc.children}
|
||||
parentNumbering=""
|
||||
onContentClicked={onContentClicked}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -268,27 +308,24 @@ interface HeaderProps {
|
|||
|
||||
const Header = ({ level, title, slug }: HeaderProps): JSX.Element => {
|
||||
const isHoverable = useDeviceSupportsHover();
|
||||
const innerComponent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div className="ml-10 flex place-items-center gap-4">
|
||||
{title === "* * *" ? (
|
||||
<div className="mt-8 mb-12 space-x-3 text-dark">
|
||||
<Ico icon={Icon.Emergency} />
|
||||
<Ico icon={Icon.Emergency} />
|
||||
<Ico icon={Icon.Emergency} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-headers">{title}</div>
|
||||
)}
|
||||
<AnchorShare
|
||||
className={cIf(isHoverable, "opacity-0 transition-opacity group-hover:opacity-100")}
|
||||
id={slug}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
[isHoverable, slug, title]
|
||||
const innerComponent = (
|
||||
<>
|
||||
<div className="ml-10 flex place-items-center gap-4">
|
||||
{title === "* * *" ? (
|
||||
<div className="mb-12 mt-8 space-x-3 text-dark">
|
||||
<Ico icon="emergency" />
|
||||
<Ico icon="emergency" />
|
||||
<Ico icon="emergency" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-headers">{title}</div>
|
||||
)}
|
||||
<AnchorShare
|
||||
className={cIf(isHoverable, "opacity-0 transition-opacity group-hover:opacity-100")}
|
||||
id={slug}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
switch (level) {
|
||||
|
@ -341,15 +378,15 @@ interface LevelProps {
|
|||
tocchildren: TocInterface[];
|
||||
parentNumbering: string;
|
||||
allowIntersection?: boolean;
|
||||
onContentClicked?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
const TocLevel = ({
|
||||
tocchildren,
|
||||
parentNumbering,
|
||||
allowIntersection = true,
|
||||
onContentClicked,
|
||||
}: LevelProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
|
||||
const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
|
||||
const currentIntersection = useIntersectionList(ids);
|
||||
|
||||
|
@ -363,14 +400,15 @@ const TocLevel = ({
|
|||
cIf(allowIntersection && currentIntersection === childIndex, "text-dark")
|
||||
)}>
|
||||
<span className="text-dark">{`${parentNumbering}${childIndex + 1}.`}</span>{" "}
|
||||
<a onClick={async () => router.replace(`#${child.slug}`)}>
|
||||
<Link href={`#${child.slug}`} linkStyled onClick={onContentClicked}>
|
||||
{<abbr title={child.title}>{child.title}</abbr>}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<TocLevel
|
||||
tocchildren={child.children}
|
||||
parentNumbering={`${parentNumbering}${childIndex + 1}.`}
|
||||
allowIntersection={allowIntersection && currentIntersection === childIndex}
|
||||
onContentClicked={onContentClicked}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
@ -438,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 = {
|
||||
title: title ?? "Return to top",
|
||||
slug: slugify(title),
|
||||
|
@ -469,7 +514,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
|||
h5 = -1;
|
||||
scenebreak = 0;
|
||||
} else if (h2 >= 0 && line.startsWith('<Header level="3"')) {
|
||||
toc.children[h2].children.push({
|
||||
toc.children[h2]?.children.push({
|
||||
title: getTitle(line),
|
||||
slug: getSlug(line),
|
||||
children: [],
|
||||
|
@ -479,7 +524,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
|||
h5 = -1;
|
||||
scenebreak = 0;
|
||||
} else if (h3 >= 0 && line.startsWith('<Header level="4"')) {
|
||||
toc.children[h2].children[h3].children.push({
|
||||
toc.children[h2]?.children[h3]?.children.push({
|
||||
title: getTitle(line),
|
||||
slug: getSlug(line),
|
||||
children: [],
|
||||
|
@ -488,7 +533,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
|||
h5 = -1;
|
||||
scenebreak = 0;
|
||||
} else if (h4 >= 0 && line.startsWith('<Header level="5"')) {
|
||||
toc.children[h2].children[h3].children[h4].children.push({
|
||||
toc.children[h2]?.children[h3]?.children[h4]?.children.push({
|
||||
title: getTitle(line),
|
||||
slug: getSlug(line),
|
||||
children: [],
|
||||
|
@ -496,7 +541,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
|||
h5++;
|
||||
scenebreak = 0;
|
||||
} else if (h5 >= 0 && line.startsWith('<Header level="6"')) {
|
||||
toc.children[h2].children[h3].children[h4].children[h5].children.push({
|
||||
toc.children[h2]?.children[h3]?.children[h4]?.children[h5]?.children.push({
|
||||
title: getTitle(line),
|
||||
slug: getSlug(line),
|
||||
children: [],
|
||||
|
@ -512,18 +557,19 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
|
|||
};
|
||||
|
||||
if (h5 >= 0) {
|
||||
toc.children[h2].children[h3].children[h4].children[h5].children.push(child);
|
||||
toc.children[h2]?.children[h3]?.children[h4]?.children[h5]?.children.push(child);
|
||||
} else if (h4 >= 0) {
|
||||
toc.children[h2].children[h3].children[h4].children.push(child);
|
||||
toc.children[h2]?.children[h3]?.children[h4]?.children.push(child);
|
||||
} else if (h3 >= 0) {
|
||||
toc.children[h2].children[h3].children.push(child);
|
||||
toc.children[h2]?.children[h3]?.children.push(child);
|
||||
} else if (h2 >= 0) {
|
||||
toc.children[h2].children.push(child);
|
||||
toc.children[h2]?.children.push(child);
|
||||
} else {
|
||||
toc.children.push(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (toc.children.length === 0) return undefined;
|
||||
return toc;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface MarkdownProps {
|
||||
className?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Markdown = ({ className, text }: MarkdownProps): JSX.Element => (
|
||||
<div
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked(text)) }}
|
||||
/>
|
||||
);
|
|
@ -1,9 +1,10 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { MouseEventHandler, useCallback, useMemo } from "react";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { Ico } from "components/Ico";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { DownPressable } from "components/Containers/DownPressable";
|
||||
|
@ -15,14 +16,14 @@ import { DownPressable } from "components/Containers/DownPressable";
|
|||
|
||||
interface Props {
|
||||
url: string;
|
||||
icon?: Icon;
|
||||
icon?: MaterialSymbol;
|
||||
title: string | null | undefined;
|
||||
subtitle?: string | null | undefined;
|
||||
border?: boolean;
|
||||
reduced?: boolean;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
@ -39,10 +40,7 @@ export const NavOption = ({
|
|||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const isActive = useMemo(
|
||||
() => active || router.asPath.startsWith(url),
|
||||
[active, router.asPath, url]
|
||||
);
|
||||
const isActive = active || router.asPath.startsWith(url);
|
||||
|
||||
return (
|
||||
<ToolTip
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Ico, Icon } from "components/Ico";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { Ico } from "components/Ico";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -7,7 +8,7 @@ import { isDefinedAndNotEmpty } from "helpers/others";
|
|||
*/
|
||||
|
||||
interface Props {
|
||||
icon?: Icon;
|
||||
icon?: MaterialSymbol;
|
||||
title: string | null | undefined;
|
||||
description?: string | null | undefined;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { useCallback } from "react";
|
||||
import { Icon } from "components/Ico";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { isDefined } from "helpers/others";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { cJoin } from "helpers/className";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -15,27 +13,18 @@ import { useAtomGetter } from "helpers/atoms";
|
|||
interface Props {
|
||||
href: string;
|
||||
title: string | null | undefined;
|
||||
|
||||
displayOnlyOn?: "1ColumnLayout" | "3ColumnsLayout";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
export const ReturnButton = ({ href, title, className }: Props): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
|
||||
return (
|
||||
<>
|
||||
{((is3ColumnsLayout && displayOnlyOn === "3ColumnsLayout") ||
|
||||
(!is3ColumnsLayout && displayOnlyOn === "1ColumnLayout") ||
|
||||
!isDefined(displayOnlyOn)) && (
|
||||
<div className={className}>
|
||||
<Button href={href} text={`${langui.return_to} ${title}`} icon={Icon.NavigateBefore} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className={cJoin("mx-auto w-full max-w-lg place-self-center", className)}>
|
||||
<Button href={href} text={format("return_to_x", { x: title })} icon="navigate_before" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,16 +1,17 @@
|
|||
import Markdown from "markdown-to-jsx";
|
||||
import { useCallback } from "react";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { NavOption } from "components/PanelComponents/NavOption";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { Icon } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { ColoredSvg } from "components/ColoredSvg";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||
import { Markdawn } from "components/Markdown/Markdawn";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -19,9 +20,15 @@ import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
|
|||
|
||||
export const MainPanel = (): JSX.Element => {
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { format } = useFormat();
|
||||
const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced);
|
||||
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
|
||||
const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened);
|
||||
const [isSettingsOpened, setSettingsOpened] = useAtomPair(atoms.layout.settingsOpened);
|
||||
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
|
||||
const [isDebugMenuOpened, setDebugMenuOpened] = useAtomPair(atoms.layout.debugMenuOpened);
|
||||
const isDebugMenuAvailable = useAtomGetter(atoms.layout.debugMenuAvailable);
|
||||
|
||||
const closeMainPanel = useCallback(() => setMainPanelOpened(false), [setMainPanelOpened]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -46,13 +53,16 @@ export const MainPanel = (): JSX.Element => {
|
|||
setMainPanelReduced((current) => !current);
|
||||
}}
|
||||
className="z-50 bg-light !px-2"
|
||||
icon={isMainPanelReduced ? Icon.ChevronRight : Icon.ChevronLeft}
|
||||
icon={isMainPanelReduced ? "chevron_right" : "chevron_left"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<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
|
||||
src="/icons/accords.svg"
|
||||
className={cJoin(
|
||||
|
@ -72,18 +82,46 @@ export const MainPanel = (): JSX.Element => {
|
|||
cIf(isMainPanelReduced && is3ColumnsLayout, "flex-col gap-3", "flex-row")
|
||||
)}>
|
||||
<ToolTip
|
||||
content={<h3 className="text-2xl">{langui.open_settings}</h3>}
|
||||
placement="right"
|
||||
className="text-left"
|
||||
disabled={!isMainPanelReduced}>
|
||||
content={<h3 className="text-2xl">{format("open_settings")}</h3>}
|
||||
placement={isMainPanelReduced ? "right" : "top"}>
|
||||
<Button
|
||||
active={isSettingsOpened}
|
||||
onClick={() => {
|
||||
closeMainPanel();
|
||||
setSettingsOpened(true);
|
||||
sendAnalytics("Settings", "Open settings");
|
||||
}}
|
||||
icon={Icon.Settings}
|
||||
icon="discover_tune"
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={<h3 className="text-2xl">{format("open_search")}</h3>}
|
||||
placement={isMainPanelReduced ? "right" : "top"}>
|
||||
<Button
|
||||
active={isSearchOpened}
|
||||
onClick={() => {
|
||||
closeMainPanel();
|
||||
setSearchOpened(true);
|
||||
sendAnalytics("Search", "Open search");
|
||||
}}
|
||||
icon="search"
|
||||
/>
|
||||
</ToolTip>
|
||||
{isDebugMenuAvailable && (
|
||||
<ToolTip
|
||||
content={<h3 className="text-2xl">Debug menu</h3>}
|
||||
placement={isMainPanelReduced ? "right" : "top"}>
|
||||
<Button
|
||||
active={isDebugMenuOpened}
|
||||
onClick={() => {
|
||||
closeMainPanel();
|
||||
setDebugMenuOpened(true);
|
||||
sendAnalytics("Debug", "Open debug menu");
|
||||
}}
|
||||
icon="bug_report"
|
||||
/>
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -92,89 +130,89 @@ export const MainPanel = (): JSX.Element => {
|
|||
|
||||
<NavOption
|
||||
url="/library"
|
||||
icon={Icon.LibraryBooks}
|
||||
title={langui.library}
|
||||
subtitle={langui.library_short_description}
|
||||
icon="auto_stories"
|
||||
title={format("library")}
|
||||
subtitle={format("library_short_description")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
url="/contents"
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
subtitle={langui.contents_short_description}
|
||||
icon="workspaces"
|
||||
title={format("contents")}
|
||||
subtitle={format("contents_short_description")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
url="/wiki"
|
||||
icon={Icon.TravelExplore}
|
||||
title={langui.wiki}
|
||||
subtitle={langui.wiki_short_description}
|
||||
icon="travel_explore"
|
||||
title={format("wiki")}
|
||||
subtitle={format("wiki_short_description")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
url="/chronicles"
|
||||
icon={Icon.WatchLater}
|
||||
title={langui.chronicles}
|
||||
subtitle={langui.chronicles_short_description}
|
||||
icon="schedule"
|
||||
title={format("chronicles")}
|
||||
subtitle={format("chronicles_short_description")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<NavOption
|
||||
url="/news"
|
||||
icon={Icon.Feed}
|
||||
title={langui.news}
|
||||
icon="newspaper"
|
||||
title={format("news")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
{/*
|
||||
<NavOption
|
||||
url="/merch"
|
||||
icon={Icon.Store}
|
||||
title={langui.merch}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
/>
|
||||
*/}
|
||||
|
||||
<NavOption
|
||||
url="https://gallery.accords-library.com/posts/"
|
||||
icon={Icon.Collections}
|
||||
title={langui.gallery}
|
||||
icon="perm_media"
|
||||
title={format("gallery")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
url="/archives"
|
||||
icon={Icon.Inventory2}
|
||||
title={langui.archives}
|
||||
icon="save"
|
||||
title={format("archives")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
url="/about-us"
|
||||
icon={Icon.Info}
|
||||
title={langui.about_us}
|
||||
icon="info"
|
||||
title={format("about_us")}
|
||||
reduced={isMainPanelReduced && is3ColumnsLayout}
|
||||
onClick={closeMainPanel}
|
||||
/>
|
||||
|
||||
{(!isMainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />}
|
||||
|
||||
<div className={cJoin("text-center", cIf(isMainPanelReduced && is3ColumnsLayout, "hidden"))}>
|
||||
{isDefinedAndNotEmpty(langui.licensing_notice) && (
|
||||
{isDefinedAndNotEmpty(format("licensing_notice")) && (
|
||||
<p>
|
||||
<Markdown>{langui.licensing_notice}</Markdown>
|
||||
<Markdawn text={format("licensing_notice")} />
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 mb-8 grid place-content-center">
|
||||
<a
|
||||
<div className="mb-8 mt-4 grid place-content-center">
|
||||
<Link
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit license")}
|
||||
aria-label="Read more about the license we use for this website"
|
||||
className="group grid grid-flow-col place-content-center gap-1 transition-filter"
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/">
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-6 w-6 bg-black group-hover:bg-dark"
|
||||
src="/icons/creative-commons-brands.svg"
|
||||
|
@ -187,47 +225,44 @@ export const MainPanel = (): JSX.Element => {
|
|||
className="h-6 w-6 bg-black group-hover:bg-dark"
|
||||
src="/icons/creative-commons-sa-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
{isDefinedAndNotEmpty(langui.copyright_notice) && (
|
||||
{isDefinedAndNotEmpty(format("copyright_notice")) && (
|
||||
<p>
|
||||
<Markdown>{langui.copyright_notice}</Markdown>
|
||||
<Markdawn text={format("copyright_notice")} />
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
|
||||
<a
|
||||
<div className="mb-4 mt-12 grid h-4 grid-flow-col place-content-center gap-8">
|
||||
<Link
|
||||
aria-label="Browse our GitHub repository, which include this website source code"
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit GitHub")}
|
||||
href="https://github.com/Accords-Library"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-10 w-10 bg-black hover:bg-dark"
|
||||
src="/icons/github-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Follow us on Twitter"
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit Twitter")}
|
||||
href="https://twitter.com/AccordsLibrary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-10 w-10 bg-black hover:bg-dark"
|
||||
src="/icons/twitter-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Join our Discord server!"
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit Discord")}
|
||||
href="/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-10 w-10 bg-black hover:bg-dark"
|
||||
src="/icons/discord-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { useMemo } from "react";
|
||||
import UAParser from "ua-parser-js";
|
||||
import { useIsClient, useSessionStorage } from "usehooks-ts";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { Popup } from "components/Containers/Popup";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
|
||||
export const SafariPopup = (): JSX.Element => {
|
||||
const [hasDisgardedSafariWarning, setHasDisgardedSafariWarning] = useSessionStorage(
|
||||
"hasDisgardedSafariWarning",
|
||||
false
|
||||
);
|
||||
|
||||
const isClient = useIsClient();
|
||||
const isSafari = useMemo<boolean>(() => {
|
||||
if (isClient) {
|
||||
const parser = new UAParser();
|
||||
return parser.getBrowser().name === "Safari" || parser.getOS().name === "iOS";
|
||||
}
|
||||
return false;
|
||||
}, [isClient]);
|
||||
|
||||
return (
|
||||
<Popup isVisible={isSafari && !hasDisgardedSafariWarning}>
|
||||
<h1 className="text-2xl">Hi, you are using Safari!</h1>
|
||||
<p className="max-w-lg text-center">
|
||||
In most cases this wouldn’t be a problem but our website is—for some obscure
|
||||
reason—performing terribly on Safari (WebKit). Because of that, we have decided to display
|
||||
this message instead of letting you have a slow and painful experience. We are looking into
|
||||
the problem, and are hoping to fix this soon.
|
||||
</p>
|
||||
<p>In the meanwhile, if you are using an iPhone/iPad, please try using another device.</p>
|
||||
<p>If you are on macOS, please use another browser such as Firefox or Chrome.</p>
|
||||
|
||||
<Button
|
||||
text="Let me in regardless"
|
||||
className="mt-8"
|
||||
onClick={() => {
|
||||
setHasDisgardedSafariWarning(true);
|
||||
sendAnalytics("Safari", "Disgard warning");
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,535 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import { MaterialSymbol } from "material-symbols";
|
||||
import { Popup } from "components/Containers/Popup";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||
import { TextInput } from "components/Inputs/TextInput";
|
||||
import {
|
||||
containsHighlight,
|
||||
CustomSearchResponse,
|
||||
filterHitsWithHighlight,
|
||||
meiliMultiSearch,
|
||||
} from "helpers/search";
|
||||
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import {
|
||||
MeiliContent,
|
||||
MeiliIndices,
|
||||
MeiliLibraryItem,
|
||||
MeiliPost,
|
||||
MeiliVideo,
|
||||
MeiliWeapon,
|
||||
MeiliWikiPage,
|
||||
} from "shared/meilisearch-graphql-typings/meiliTypes";
|
||||
import { getVideoThumbnailURL } from "helpers/videos";
|
||||
import { UpPressable } from "components/Containers/UpPressable";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { Ico } from "components/Ico";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
const SEARCH_LIMIT = 8;
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ 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 => {
|
||||
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
|
||||
const [query, setQuery] = useState("");
|
||||
const {
|
||||
format,
|
||||
formatCategory,
|
||||
formatContentType,
|
||||
formatWikiTag,
|
||||
formatLibraryItemSubType,
|
||||
formatWeaponType,
|
||||
} = useFormat();
|
||||
const [multiResult, setMultiResult] = useState<MultiResult>({});
|
||||
|
||||
const fetchSearchResults = useCallback((q: string) => {
|
||||
const fetchMultiResult = async () => {
|
||||
const searchResults = (
|
||||
await meiliMultiSearch([
|
||||
{
|
||||
indexUid: MeiliIndices.LIBRARY_ITEM,
|
||||
q,
|
||||
limit: SEARCH_LIMIT,
|
||||
attributesToRetrieve: [
|
||||
"title",
|
||||
"subtitle",
|
||||
"descriptions",
|
||||
"id",
|
||||
"slug",
|
||||
"thumbnail",
|
||||
"release_date",
|
||||
"price",
|
||||
"categories",
|
||||
"metadata",
|
||||
],
|
||||
attributesToHighlight: ["title", "subtitle", "descriptions"],
|
||||
attributesToCrop: ["descriptions"],
|
||||
},
|
||||
{
|
||||
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?");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setMultiResult(result);
|
||||
};
|
||||
|
||||
if (q === "") {
|
||||
setMultiResult({});
|
||||
} else {
|
||||
fetchMultiResult();
|
||||
}
|
||||
|
||||
setQuery(q);
|
||||
}, []);
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
isVisible={isSearchOpened}
|
||||
onCloseRequest={() => {
|
||||
setSearchOpened(false);
|
||||
sendAnalytics("Search", "Close search");
|
||||
}}
|
||||
onOpen={() => searchInputRef.current?.focus()}
|
||||
fillViewport>
|
||||
<h2 className="inline-flex place-items-center gap-2 text-2xl">
|
||||
<Ico icon="search" isFilled />
|
||||
{format("search")}
|
||||
</h2>
|
||||
<TextInput
|
||||
ref={searchInputRef}
|
||||
onChange={fetchSearchResults}
|
||||
value={query}
|
||||
placeholder={format("search_placeholder")}
|
||||
/>
|
||||
|
||||
<div className="flex w-full flex-wrap gap-12 gap-x-16">
|
||||
{isDefined(multiResult.libraryItems) && (
|
||||
<SearchResultSection
|
||||
title={format("library")}
|
||||
icon="auto_stories"
|
||||
href={`/library?page=1&query=${query}\
|
||||
&sort=0&primary=true&secondary=true&subitems=true&status=all`}
|
||||
totalHits={multiResult.libraryItems.estimatedTotalHits}>
|
||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||
{multiResult.libraryItems.hits.map((item) => (
|
||||
<TranslatedPreviewCard
|
||||
key={item.id}
|
||||
className="w-56"
|
||||
href={`/library/${item.slug}`}
|
||||
onClick={() => setSearchOpened(false)}
|
||||
translations={filterHasAttributes(item._formatted.descriptions, [
|
||||
"language.data.attributes.code",
|
||||
]).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
description: containsHighlight(translation.description)
|
||||
? translation.description
|
||||
: undefined,
|
||||
}))}
|
||||
fallback={{ title: item._formatted.title, subtitle: item._formatted.subtitle }}
|
||||
thumbnail={item.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible
|
||||
topChips={
|
||||
item.metadata && item.metadata.length > 0 && item.metadata[0]
|
||||
? [formatLibraryItemSubType(item.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.release_date,
|
||||
price: item.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SearchResultSection>
|
||||
)}
|
||||
|
||||
{isDefined(multiResult.contents) && (
|
||||
<SearchResultSection
|
||||
title={format("contents")}
|
||||
icon="workspaces"
|
||||
href={`/contents/all?page=1&query=${query}&sort=0`}
|
||||
totalHits={multiResult.contents.estimatedTotalHits}>
|
||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||
{multiResult.contents.hits.map((item) => (
|
||||
<TranslatedPreviewCard
|
||||
key={item.id}
|
||||
className="w-56"
|
||||
href={`/contents/${item.slug}`}
|
||||
onClick={() => setSearchOpened(false)}
|
||||
translations={filterHasAttributes(item._formatted.translations, [
|
||||
"language.data.attributes.code",
|
||||
]).map(({ displayable_description, language, ...otherAttributes }) => ({
|
||||
...otherAttributes,
|
||||
description: containsHighlight(displayable_description)
|
||||
? displayable_description
|
||||
: undefined,
|
||||
language: language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.slug) }}
|
||||
thumbnail={item.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.type?.data?.attributes
|
||||
? [formatContentType(item.type.data.attributes.slug)]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SearchResultSection>
|
||||
)}
|
||||
|
||||
{isDefined(multiResult.wikiPages) && (
|
||||
<SearchResultSection
|
||||
title={format("wiki")}
|
||||
icon="travel_explore"
|
||||
href={`/wiki?page=1&query=${query}`}
|
||||
totalHits={multiResult.wikiPages.estimatedTotalHits}>
|
||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||
{multiResult.wikiPages.hits.map((item) => (
|
||||
<TranslatedPreviewCard
|
||||
key={item.id}
|
||||
className="w-56"
|
||||
href={`/wiki/${item.slug}`}
|
||||
onClick={() => setSearchOpened(false)}
|
||||
translations={filterHasAttributes(item._formatted.translations, [
|
||||
"language.data.attributes.code",
|
||||
]).map(
|
||||
({
|
||||
aliases,
|
||||
summary,
|
||||
displayable_description,
|
||||
language,
|
||||
...otherAttributes
|
||||
}) => ({
|
||||
...otherAttributes,
|
||||
subtitle:
|
||||
aliases && aliases.length > 0
|
||||
? aliases.map((alias) => alias?.alias).join("・")
|
||||
: undefined,
|
||||
description: containsHighlight(displayable_description)
|
||||
? displayable_description
|
||||
: summary,
|
||||
language: language.data.attributes.code,
|
||||
})
|
||||
)}
|
||||
fallback={{ title: prettySlug(item.slug) }}
|
||||
thumbnail={item.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio={"4/3"}
|
||||
thumbnailRounded
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible
|
||||
topChips={filterHasAttributes(item.tags?.data, ["attributes"]).map((tag) =>
|
||||
formatWikiTag(tag.attributes.slug)
|
||||
)}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SearchResultSection>
|
||||
)}
|
||||
|
||||
{isDefined(multiResult.posts) && (
|
||||
<SearchResultSection
|
||||
title={format("news")}
|
||||
icon="newspaper"
|
||||
href={`/news?page=1&query=${query}`}
|
||||
totalHits={multiResult.posts.estimatedTotalHits}>
|
||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||
{multiResult.posts.hits.map((item) => (
|
||||
<TranslatedPreviewCard
|
||||
className="w-56"
|
||||
key={item.id}
|
||||
href={`/news/${item.slug}`}
|
||||
onClick={() => setSearchOpened(false)}
|
||||
translations={filterHasAttributes(item._formatted.translations, [
|
||||
"language.data.attributes.code",
|
||||
]).map(({ excerpt, displayable_description, language, ...otherAttributes }) => ({
|
||||
...otherAttributes,
|
||||
description: containsHighlight(displayable_description)
|
||||
? displayable_description
|
||||
: excerpt,
|
||||
language: language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.slug) }}
|
||||
thumbnail={item.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"]).map(
|
||||
(category) => formatCategory(category.attributes.slug)
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.date,
|
||||
releaseDateFormat: "long",
|
||||
position: "Top",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SearchResultSection>
|
||||
)}
|
||||
|
||||
{isDefined(multiResult.videos) && (
|
||||
<SearchResultSection
|
||||
title={format("videos")}
|
||||
icon="movie"
|
||||
href={`/archives/videos?page=1&query=${query}&sort=1&gone=`}
|
||||
totalHits={multiResult.videos.estimatedTotalHits}>
|
||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-8">
|
||||
{multiResult.videos.hits.map((item) => (
|
||||
<PreviewCard
|
||||
className="w-56"
|
||||
key={item.uid}
|
||||
href={`/archives/videos/v/${item.uid}`}
|
||||
onClick={() => setSearchOpened(false)}
|
||||
title={item._formatted.title}
|
||||
thumbnail={getVideoThumbnailURL(item.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible
|
||||
metadata={{
|
||||
releaseDate: item.published_date,
|
||||
views: item.views,
|
||||
author: item._formatted.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
description={
|
||||
item._matchesPosition.description &&
|
||||
item._matchesPosition.description.length > 0
|
||||
? item._formatted.description
|
||||
: undefined
|
||||
}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.duration,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface SearchResultSectionProps {
|
||||
title?: string | null;
|
||||
icon: MaterialSymbol;
|
||||
href: string;
|
||||
totalHits?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SearchResultSection = ({
|
||||
title,
|
||||
icon,
|
||||
href,
|
||||
totalHits,
|
||||
children,
|
||||
}: SearchResultSectionProps) => {
|
||||
const { format } = useFormat();
|
||||
const setSearchOpened = useAtomSetter(atoms.layout.searchOpened);
|
||||
return (
|
||||
<>
|
||||
{isDefined(totalHits) && totalHits > 0 && (
|
||||
<div>
|
||||
<div className="mb-6 grid place-content-start">
|
||||
<UpPressable
|
||||
className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4"
|
||||
href={href}
|
||||
onClick={() => setSearchOpened(false)}>
|
||||
<Ico icon={icon} className="!text-3xl" isFilled={false} />
|
||||
<div>
|
||||
<p className="font-headers text-lg">{title}</p>
|
||||
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
|
||||
<p className="text-sm">
|
||||
({format("showing_x_out_of_y_results", { x: SEARCH_LIMIT, y: totalHits })})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</UpPressable>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,5 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "components/Ico";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||
import { OrderableList } from "components/Inputs/OrderableList";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
|
@ -9,11 +7,19 @@ import { TextInput } from "components/Inputs/TextInput";
|
|||
import { Popup } from "components/Containers/Popup";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
import { cJoin, cIf } from "helpers/className";
|
||||
import { prettyLanguage } from "helpers/formatters";
|
||||
import { filterHasAttributes, isDefined } from "helpers/others";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
import { ThemeMode } from "contexts/settings";
|
||||
import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||
import { PerfMode, ThemeMode } from "contexts/settings";
|
||||
import { Ico } from "components/Ico";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
export const SettingsPopup = (): JSX.Element => {
|
||||
const [preferredLanguages, setPreferredLanguages] = useAtomPair(
|
||||
|
@ -25,33 +31,26 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
const [fontSize, setFontSize] = useAtomPair(atoms.settings.fontSize);
|
||||
const [playerName, setPlayerName] = useAtomPair(atoms.settings.playerName);
|
||||
const [themeMode, setThemeMode] = useAtomPair(atoms.settings.themeMode);
|
||||
const setPerfMode = useAtomSetter(atoms.settings.perfMode);
|
||||
const perfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const isPerfModeToggleable = useAtomGetter(atoms.settings.isPerfModeToggleable);
|
||||
|
||||
const languages = useAtomGetter(atoms.localData.languages);
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { format, formatLanguage } = useFormat();
|
||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const currencyOptions = useMemo(
|
||||
() =>
|
||||
filterHasAttributes(currencies, ["attributes"] as const).map(
|
||||
(currentCurrency) => currentCurrency.attributes.code
|
||||
),
|
||||
[currencies]
|
||||
const currencyOptions = filterHasAttributes(currencies, ["attributes"]).map(
|
||||
(currentCurrency) => currentCurrency.attributes.code
|
||||
);
|
||||
|
||||
const [currencySelect, setCurrencySelect] = useState<number>(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(currency)) setCurrencySelect(currencyOptions.indexOf(currency));
|
||||
}, [currency, currencyOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currencySelect >= 0) setCurrency(currencyOptions[currencySelect]);
|
||||
}, [currencyOptions, currencySelect, setCurrency]);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
isVisible={isSettingsOpened}
|
||||
|
@ -59,7 +58,10 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
setSettingsOpened(false);
|
||||
sendAnalytics("Settings", "Close settings");
|
||||
}}>
|
||||
<h2 className="text-2xl">{langui.settings}</h2>
|
||||
<h2 className="inline-flex place-items-center gap-2 text-2xl">
|
||||
<Ico icon="discover_tune" isFilled />
|
||||
{format("settings")}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className={cJoin(
|
||||
|
@ -68,21 +70,21 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
)}>
|
||||
{router.locales && (
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.languages}</h3>
|
||||
<h3 className="text-xl">{format("language", { count: preferredLanguages.length })}</h3>
|
||||
{preferredLanguages.length > 0 && (
|
||||
<OrderableList
|
||||
items={preferredLanguages.map((locale) => ({
|
||||
code: locale,
|
||||
name: prettyLanguage(locale, languages),
|
||||
name: formatLanguage(locale),
|
||||
}))}
|
||||
insertLabels={[
|
||||
{
|
||||
insertAt: 0,
|
||||
name: langui.primary_language ?? "Primary language",
|
||||
name: format("primary_language"),
|
||||
},
|
||||
{
|
||||
insertAt: 1,
|
||||
name: langui.secondary_language ?? "Secondary languages",
|
||||
name: format("secondary_language"),
|
||||
},
|
||||
]}
|
||||
onChange={(items) => {
|
||||
|
@ -100,7 +102,12 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
cIf(!is1ColumnLayout, "grid-cols-2")
|
||||
)}>
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.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
|
||||
buttonsProps={[
|
||||
{
|
||||
|
@ -109,7 +116,7 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
sendAnalytics("Settings", "Change theme (light)");
|
||||
},
|
||||
active: themeMode === ThemeMode.Light,
|
||||
text: langui.light,
|
||||
text: format("light"),
|
||||
},
|
||||
{
|
||||
onClick: () => {
|
||||
|
@ -117,7 +124,7 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
sendAnalytics("Settings", "Change theme (auto)");
|
||||
},
|
||||
active: themeMode === ThemeMode.Auto,
|
||||
text: langui.auto,
|
||||
text: format("auto"),
|
||||
},
|
||||
{
|
||||
onClick: () => {
|
||||
|
@ -125,21 +132,24 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
sendAnalytics("Settings", "Change theme (dark)");
|
||||
},
|
||||
active: themeMode === ThemeMode.Dark,
|
||||
text: langui.dark,
|
||||
text: format("dark"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.currency}</h3>
|
||||
<h3 className="text-xl">{format("currency")}</h3>
|
||||
<div>
|
||||
<Select
|
||||
options={currencyOptions}
|
||||
value={currencySelect}
|
||||
onChange={(newCurrency) => {
|
||||
setCurrencySelect(newCurrency);
|
||||
sendAnalytics("Settings", `Change currency (${currencyOptions[newCurrency]})}`);
|
||||
const newCurrencyName = currencyOptions[newCurrency];
|
||||
if (isDefined(newCurrencyName)) {
|
||||
setCurrency(newCurrencyName);
|
||||
sendAnalytics("Settings", `Change currency (${currencyOptions[newCurrency]})`);
|
||||
}
|
||||
}}
|
||||
className="w-28"
|
||||
/>
|
||||
|
@ -147,7 +157,7 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.font_size}</h3>
|
||||
<h3 className="text-xl">{format("font_size")}</h3>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
|
@ -160,7 +170,7 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
})}%)`
|
||||
);
|
||||
},
|
||||
icon: Icon.TextDecrease,
|
||||
icon: "text_decrease",
|
||||
},
|
||||
{
|
||||
onClick: () => {
|
||||
|
@ -181,40 +191,50 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
})}%)`
|
||||
);
|
||||
},
|
||||
icon: Icon.TextIncrease,
|
||||
icon: "text_increase",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.font}</h3>
|
||||
<h3 className="text-xl">{format("font")}</h3>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
active={!isDyslexic}
|
||||
onClick={() => {
|
||||
setDyslexic(false);
|
||||
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
||||
}}
|
||||
className="font-zenMaruGothic"
|
||||
text="Zen Maru Gothic"
|
||||
/>
|
||||
<Button
|
||||
active={isDyslexic}
|
||||
onClick={() => {
|
||||
setDyslexic(true);
|
||||
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
||||
}}
|
||||
className="font-openDyslexic"
|
||||
text="OpenDyslexic"
|
||||
<ButtonGroup
|
||||
vertical
|
||||
buttonsProps={[
|
||||
{
|
||||
active: !isDyslexic,
|
||||
onClick: () => {
|
||||
setDyslexic(false);
|
||||
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
|
||||
},
|
||||
className: "font-zenMaruGothic",
|
||||
text: "Zen Maru Gothic",
|
||||
},
|
||||
{
|
||||
active: isDyslexic,
|
||||
onClick: () => {
|
||||
setDyslexic(true);
|
||||
sendAnalytics("Settings", "Change font (OpenDyslexic)");
|
||||
},
|
||||
className: "font-openDyslexic",
|
||||
text: "OpenDyslexic",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.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
|
||||
placeholder="<player>"
|
||||
placeholder="(player)"
|
||||
className="w-48"
|
||||
value={playerName}
|
||||
onChange={(newName) => {
|
||||
|
@ -223,6 +243,20 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<div className="flex place-content-center place-items-center gap-1">
|
||||
<h3 className="text-xl">{format("performance_mode")}</h3>
|
||||
<ToolTip content={format("performance_mode_tooltip")} placement="top">
|
||||
<Ico icon="info" />
|
||||
</ToolTip>
|
||||
</div>
|
||||
<Switch
|
||||
value={perfModeEnabled}
|
||||
onClick={() => setPerfMode(perfModeEnabled ? PerfMode.Off : PerfMode.On)}
|
||||
disabled={!isPerfModeToggleable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
|
|
@ -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,20 +1,20 @@
|
|||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
||||
import { Chip } from "./Chip";
|
||||
import { HorizontalLine } from "./HorizontalLine";
|
||||
import { Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
||||
import { getTocFromMarkdawn, Markdawn, TableOfContents } from "./Markdown/Markdawn";
|
||||
import { ReturnButton } from "./PanelComponents/ReturnButton";
|
||||
import { ContentPanel } from "./Containers/ContentPanel";
|
||||
import { SubPanel } from "./Containers/SubPanel";
|
||||
import { RecorderChip } from "./RecorderChip";
|
||||
import { ThumbnailHeader } from "./ThumbnailHeader";
|
||||
import { ToolTip } from "./ToolTip";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { PostWithTranslations } from "types/types";
|
||||
import { filterHasAttributes, getStatusDescription } from "helpers/others";
|
||||
import { filterHasAttributes, isDefined } from "helpers/asserts";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { ElementsSeparator } from "helpers/component";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Credits } from "components/Credits";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -49,7 +49,10 @@ export const PostPage = ({
|
|||
displayTitle = true,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { formatCategory } = useFormat();
|
||||
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||
items: post.translations,
|
||||
languageExtractor: useCallback(
|
||||
|
@ -59,140 +62,75 @@ export const PostPage = ({
|
|||
),
|
||||
});
|
||||
|
||||
const { thumbnail, body, title, excerpt } = useMemo(
|
||||
() => ({
|
||||
thumbnail:
|
||||
selectedTranslation?.thumbnail?.data?.attributes ?? post.thumbnail?.data?.attributes,
|
||||
body: selectedTranslation?.body ?? "",
|
||||
title: selectedTranslation?.title ?? prettySlug(post.slug),
|
||||
excerpt: selectedTranslation?.excerpt ?? "",
|
||||
}),
|
||||
[post.slug, post.thumbnail, selectedTranslation]
|
||||
);
|
||||
const thumbnail =
|
||||
selectedTranslation?.thumbnail?.data?.attributes ?? post.thumbnail?.data?.attributes;
|
||||
const body = selectedTranslation?.body ?? "";
|
||||
const title = selectedTranslation?.title ?? prettySlug(post.slug);
|
||||
const excerpt = selectedTranslation?.excerpt ?? "";
|
||||
|
||||
const subPanel = useMemo(
|
||||
() =>
|
||||
returnHref || returnTitle || displayCredits || displayToc ? (
|
||||
<SubPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton href={returnHref} title={returnTitle} displayOnlyOn={"3ColumnsLayout"} />
|
||||
)}
|
||||
const toc = getTocFromMarkdawn(body, title);
|
||||
|
||||
{displayCredits && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
|
||||
{selectedTranslation && (
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedTranslation.status, langui)}
|
||||
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>
|
||||
) : undefined,
|
||||
[
|
||||
body,
|
||||
displayCredits,
|
||||
displayToc,
|
||||
langui,
|
||||
post.authors,
|
||||
returnHref,
|
||||
returnTitle,
|
||||
selectedTranslation,
|
||||
title,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayThumbnailHeader ? (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
categories={post.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLanguageSwitcher && (
|
||||
<div className="grid place-content-end place-items-start">
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
</div>
|
||||
)}
|
||||
{displayTitle && (
|
||||
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">{title}</h1>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{prependBody}
|
||||
{body && (
|
||||
<>
|
||||
{displayThumbnailHeader && <HorizontalLine />}
|
||||
<Markdawn text={body} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{appendBody}
|
||||
</ContentPanel>
|
||||
const subPanelElems = [
|
||||
returnHref && returnTitle && !is1ColumnLayout && (
|
||||
<ReturnButton href={returnHref} title={returnTitle} />
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
appendBody,
|
||||
body,
|
||||
displayLanguageSwitcher,
|
||||
displayThumbnailHeader,
|
||||
displayTitle,
|
||||
excerpt,
|
||||
languageSwitcherProps,
|
||||
post.categories,
|
||||
prependBody,
|
||||
returnHref,
|
||||
returnTitle,
|
||||
thumbnail,
|
||||
title,
|
||||
]
|
||||
|
||||
displayCredits && <Credits status={selectedTranslation?.status} authors={post.authors?.data} />,
|
||||
|
||||
displayToc && isDefined(toc) && (
|
||||
<TableOfContents toc={toc} onContentClicked={() => setSubPanelOpened(false)} />
|
||||
),
|
||||
];
|
||||
|
||||
const subPanel =
|
||||
subPanelElems.filter(Boolean).length > 0 ? (
|
||||
<SubPanel>
|
||||
<ElementsSeparator>{subPanelElems}</ElementsSeparator>
|
||||
</SubPanel>
|
||||
) : undefined;
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
{is1ColumnLayout && returnHref && returnTitle && (
|
||||
<ReturnButton href={returnHref} title={returnTitle} className="mb-10" />
|
||||
)}
|
||||
|
||||
{displayThumbnailHeader ? (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
categories={filterHasAttributes(post.categories?.data, ["attributes"]).map((category) =>
|
||||
formatCategory(category.attributes.slug)
|
||||
)}
|
||||
releaseDate={post.date}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLanguageSwitcher && (
|
||||
<div className="grid place-content-end place-items-start">
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
</div>
|
||||
)}
|
||||
{displayTitle && (
|
||||
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">{title}</h1>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{prependBody}
|
||||
{body && <Markdawn text={body} />}
|
||||
|
||||
{appendBody}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout {...otherProps} contentPanel={contentPanel} subPanel={subPanel} />;
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Chip } from "./Chip";
|
||||
import { Ico, Icon } from "./Ico";
|
||||
import { Img } from "./Img";
|
||||
import { UpPressable } from "./Containers/UpPressable";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { Markdown } from "./Markdown/Markdown";
|
||||
import { Chip } from "components/Chip";
|
||||
import { Ico } from "components/Ico";
|
||||
import { Img } from "components/Img";
|
||||
import { UpPressable } from "components/Containers/UpPressable";
|
||||
import { DatePickerFragment, PricePickerFragment, UploadImageFragment } from "graphql/generated";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { prettyDate, prettyDuration, prettyPrice, prettyShortenNumber } from "helpers/formatters";
|
||||
import { prettyDuration, prettyShortenNumber } from "helpers/formatters";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -23,6 +25,7 @@ interface Props {
|
|||
thumbnail?: UploadImageFragment | string | null | undefined;
|
||||
thumbnailAspectRatio?: string;
|
||||
thumbnailForceAspectRatio?: boolean;
|
||||
thumbnailFitMethod?: "contain" | "cover";
|
||||
thumbnailRounded?: boolean;
|
||||
href: string;
|
||||
pre_title?: string | null | undefined;
|
||||
|
@ -48,6 +51,8 @@ interface Props {
|
|||
}
|
||||
| { __typename: "anotherHoverlayName" };
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
@ -57,6 +62,7 @@ export const PreviewCard = ({
|
|||
thumbnail,
|
||||
thumbnailAspectRatio = "4/3",
|
||||
thumbnailForceAspectRatio = false,
|
||||
thumbnailFitMethod = "cover",
|
||||
thumbnailRounded = true,
|
||||
pre_title,
|
||||
title,
|
||||
|
@ -68,51 +74,55 @@ export const PreviewCard = ({
|
|||
metadata,
|
||||
hoverlay,
|
||||
infoAppend,
|
||||
className,
|
||||
disabled = false,
|
||||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
const currency = useAtomGetter(atoms.settings.currency);
|
||||
const currencies = useAtomGetter(atoms.localData.currencies);
|
||||
const { formatPrice, formatDate } = useFormat();
|
||||
const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled);
|
||||
const preferredCurrency = useAtomGetter(atoms.settings.currency);
|
||||
const isHoverable = useDeviceSupportsHover();
|
||||
const router = useRouter();
|
||||
|
||||
const metadataJSX = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{metadata && (metadata.releaseDate || metadata.price) && (
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
||||
{metadata.releaseDate && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyDate(metadata.releaseDate, router.locale)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.price && (
|
||||
<p className="justify-self-end text-sm">
|
||||
<Ico icon={Icon.ShoppingCart} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyPrice(metadata.price, currencies, currency)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.views && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyShortenNumber(metadata.views)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.author && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Person} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{metadata.author}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[currencies, currency, metadata, router.locale]
|
||||
const metadataJSX = (
|
||||
<>
|
||||
{metadata && (isDefined(metadata.releaseDate) || isDefined(metadata.price)) && (
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
||||
{metadata.releaseDate && (
|
||||
<p className="text-sm">
|
||||
<Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{formatDate(metadata.releaseDate)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.price && (
|
||||
<p className="justify-self-end text-sm">
|
||||
<Ico icon="shopping_cart" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{formatPrice(metadata.price, preferredCurrency)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.views && (
|
||||
<p className="text-sm">
|
||||
<Ico icon="visibility" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyShortenNumber(metadata.views)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.author && (
|
||||
<p className="text-sm">
|
||||
<Ico icon="person" className="mr-1 translate-y-[.15em] !text-base" />
|
||||
<Markdown text={metadata.author} className="inline-block" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<UpPressable className="grid items-end text-left" href={href} noBackground disabled={disabled}>
|
||||
<UpPressable
|
||||
className={cJoin("relative grid items-end text-left", className)}
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
noBackground
|
||||
disabled={disabled}>
|
||||
<div className={cJoin("group", cIf(disabled, "pointer-events-none touch-none select-none"))}>
|
||||
{thumbnail ? (
|
||||
<div
|
||||
|
@ -126,7 +136,12 @@ export const PreviewCard = ({
|
|||
thumbnailRounded,
|
||||
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}
|
||||
quality={ImageQuality.Medium}
|
||||
|
@ -135,18 +150,15 @@ export const PreviewCard = ({
|
|||
{hoverlay && hoverlay.__typename === "Video" && (
|
||||
<>
|
||||
<div
|
||||
className="group absolute inset-0 grid place-content-center bg-shade bg-opacity-0
|
||||
text-light transition-colors
|
||||
hover:bg-opacity-50">
|
||||
className="absolute inset-0 grid place-content-center rounded-t-md
|
||||
bg-shade/0 text-light transition-colors group-hover:bg-shade/50">
|
||||
<Ico
|
||||
icon={Icon.PlayCircleOutline}
|
||||
className="!text-6xl text-black opacity-0 drop-shadow-lg transition-opacity
|
||||
shadow-shade group-hover:opacity-100"
|
||||
icon="play_circle"
|
||||
className="!text-6xl text-light opacity-0 drop-shadow-lg transition-opacity
|
||||
shadow-shade group-hover:opacity-100 dark:text-black"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="absolute right-2 bottom-2 rounded-full bg-black bg-opacity-60 px-2
|
||||
text-light">
|
||||
<div className="absolute bottom-2 right-2 rounded-full bg-black/60 px-2 text-light">
|
||||
{prettyDuration(hoverlay.duration)}
|
||||
</div>
|
||||
</>
|
||||
|
@ -166,35 +178,38 @@ export const PreviewCard = ({
|
|||
"z-20 grid gap-2 p-4 transition-opacity linearbg-obi",
|
||||
cIf(
|
||||
!keepInfoVisible && isHoverable,
|
||||
`-inset-x-0.5 bottom-2 opacity-0 shadow-shade
|
||||
`-inset-x-0.5 bottom-2 opacity-0 !shadow-shade
|
||||
[border-radius:10%_10%_10%_10%_/_1%_1%_3%_3%]
|
||||
group-hover:opacity-100 hoverable:absolute hoverable:drop-shadow-lg
|
||||
group-hover:opacity-100 hoverable:absolute hoverable:shadow-lg
|
||||
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}
|
||||
{topChips && topChips.length > 0 && (
|
||||
<div
|
||||
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
||||
scrollbar-none">
|
||||
scrollbar-none">
|
||||
{topChips.map((text, index) => (
|
||||
<Chip key={index} text={text} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="my-1">
|
||||
{pre_title && <p className="mb-1 leading-none break-words">{pre_title}</p>}
|
||||
{pre_title && <Markdown text={pre_title} className="mb-1 leading-none break-words" />}
|
||||
{title && (
|
||||
<p className="font-headers text-lg font-bold leading-none break-words">{title}</p>
|
||||
<Markdown
|
||||
text={title}
|
||||
className="font-headers text-lg font-bold leading-none break-words"
|
||||
/>
|
||||
)}
|
||||
{subtitle && <p className="leading-none break-words">{subtitle}</p>}
|
||||
{subtitle && <Markdown text={subtitle} className="leading-none break-words" />}
|
||||
</div>
|
||||
{description && <p>{description}</p>}
|
||||
{description && <Markdown text={description} className="overflow-hidden break-words" />}
|
||||
{bottomChips && bottomChips.length > 0 && (
|
||||
<div
|
||||
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
||||
scrollbar-none">
|
||||
scrollbar-none">
|
||||
{bottomChips.map((text, index) => (
|
||||
<Chip key={index} className="text-sm" text={text} />
|
||||
))}
|
||||
|
|
|
@ -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/others";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ 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,11 +3,12 @@ import { Img } from "./Img";
|
|||
import { Markdawn } from "./Markdown/Markdawn";
|
||||
import { ToolTip } from "./ToolTip";
|
||||
import { Chip } from "components/Chip";
|
||||
import { RecorderChipFragment } from "graphql/generated";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { filterHasAttributes } from "helpers/others";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { filterHasAttributes, isUndefined } from "helpers/asserts";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -15,14 +16,22 @@ import { useAtomGetter } from "helpers/atoms";
|
|||
*/
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
recorder: RecorderChipFragment;
|
||||
username: string;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const RecorderChip = ({ recorder }: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
export const RecorderChip = ({ username }: Props): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const recorders = useAtomGetter(atoms.localData.recorders);
|
||||
const recorder = recorders.find((elem) => elem.attributes?.username === username)?.attributes;
|
||||
|
||||
const [selectedBioTranslation] = useSmartLanguage({
|
||||
items: recorder?.bio ?? [],
|
||||
languageExtractor: (bio) => bio.language?.data?.attributes?.code,
|
||||
});
|
||||
|
||||
if (isUndefined(recorder)) return <></>;
|
||||
|
||||
return (
|
||||
<ToolTip
|
||||
|
@ -40,25 +49,23 @@ export const RecorderChip = ({ recorder }: Props): JSX.Element => {
|
|||
<h3 className=" text-2xl">{recorder.username}</h3>
|
||||
{recorder.languages?.data && recorder.languages.data.length > 0 && (
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
<p>{langui.languages}:</p>
|
||||
{filterHasAttributes(recorder.languages.data, ["attributes"] as const).map(
|
||||
(language) => (
|
||||
<Fragment key={language.__typename}>
|
||||
<Chip text={language.attributes.code.toUpperCase()} />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
<p>{format("language", { count: recorder.languages.data.length })}:</p>
|
||||
{filterHasAttributes(recorder.languages.data, ["attributes"]).map((language) => (
|
||||
<Fragment key={language.__typename}>
|
||||
<Chip text={language.attributes.code.toUpperCase()} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{recorder.pronouns && (
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
<p>{langui.pronouns}:</p>
|
||||
<p>{format("pronouns")}:</p>
|
||||
<Chip text={recorder.pronouns} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />}
|
||||
{selectedBioTranslation?.bio && <Markdawn text={selectedBioTranslation.bio} />}
|
||||
</div>
|
||||
}
|
||||
placement="top">
|
||||
|
|
|
@ -1,244 +0,0 @@
|
|||
import { Fragment, useCallback, useEffect, useMemo, 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, Icon } from "./Ico";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { Ids } from "types/ids";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
|
||||
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(0);
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
|
||||
useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction, items]);
|
||||
|
||||
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 = useMemo(() => {
|
||||
const filteredBySearch = searchFilter();
|
||||
return filteredBySearch.filter(filteringFunction);
|
||||
}, [filteringFunction, searchFilter]);
|
||||
|
||||
const sortedItem = useMemo(
|
||||
() => filteredItems.sort(sortingFunction),
|
||||
[filteredItems, sortingFunction]
|
||||
);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const memo: Group<T>[] = [];
|
||||
|
||||
sortedItem.forEach((item) => {
|
||||
groupingFunction(item).forEach((category) => {
|
||||
const index = memo.findIndex((group) => group.name === category);
|
||||
if (index === -1) {
|
||||
memo.push({
|
||||
name: category,
|
||||
items: [item],
|
||||
totalCount: groupCountingFunction(item),
|
||||
});
|
||||
} else {
|
||||
memo[index].items.push(item);
|
||||
memo[index].totalCount += groupCountingFunction(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
return memo.sort(groupSortingFunction);
|
||||
}, [groupCountingFunction, groupSortingFunction, groupingFunction, sortedItem]);
|
||||
|
||||
const pages = useMemo(() => {
|
||||
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;
|
||||
}, [groups, paginationItemPerPage]);
|
||||
|
||||
useHotkeys("left", () => setPage((current) => current - 1), { enabled: page > 0 });
|
||||
useHotkeys("right", () => setPage((current) => current + 1), {
|
||||
enabled: page < pages.length - 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{pages.length > 1 && paginationSelectorTop && (
|
||||
<PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
|
||||
)}
|
||||
|
||||
<div className="mb-8">
|
||||
{pages[page]?.length > 0 ? (
|
||||
pages[page]?.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={`${group.totalCount} ${
|
||||
group.items.length <= 1
|
||||
? langui.result?.toLowerCase() ?? ""
|
||||
: langui.results?.toLowerCase() ?? ""
|
||||
}`}
|
||||
/>
|
||||
</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 is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
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={Icon.ChevronLeft} className="!text-[300%]" />}
|
||||
<p className="max-w-xs text-2xl">{langui.no_results_message}</p>
|
||||
{!is3ColumnsLayout && <Ico icon={Icon.ChevronRight} className="!text-[300%]" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2,12 +2,12 @@ import { Chip } from "components/Chip";
|
|||
import { Img } from "components/Img";
|
||||
import { InsetBox } from "components/Containers/InsetBox";
|
||||
import { Markdawn } from "components/Markdown/Markdawn";
|
||||
import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
|
||||
import { prettyInlineTitle, prettySlug, slugify } from "helpers/formatters";
|
||||
import { DatePickerFragment, UploadImageFragment } from "graphql/generated";
|
||||
import { prettyInlineTitle, slugify } from "helpers/formatters";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
import { filterHasAttributes } from "helpers/others";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -19,14 +19,11 @@ interface Props {
|
|||
title: string | null | undefined;
|
||||
subtitle?: string | null | undefined;
|
||||
description?: string | null | undefined;
|
||||
type?: NonNullable<
|
||||
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
|
||||
>["type"];
|
||||
categories?: NonNullable<
|
||||
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
|
||||
>["categories"];
|
||||
type?: string;
|
||||
categories?: string[];
|
||||
releaseDate?: DatePickerFragment;
|
||||
thumbnail?: UploadImageFragment | null | undefined;
|
||||
|
||||
className?: string;
|
||||
languageSwitcher?: JSX.Element;
|
||||
}
|
||||
|
||||
|
@ -41,13 +38,15 @@ export const ThumbnailHeader = ({
|
|||
categories,
|
||||
description,
|
||||
languageSwitcher,
|
||||
releaseDate,
|
||||
className,
|
||||
}: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { format, formatDate } = useFormat();
|
||||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-12 grid place-items-center gap-12">
|
||||
<div className={className}>
|
||||
<div className={"mb-12 grid place-items-center gap-12"}>
|
||||
<div className="drop-shadow-lg shadow-shade">
|
||||
{thumbnail ? (
|
||||
<Img
|
||||
|
@ -70,34 +69,37 @@ export const ThumbnailHeader = ({
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<h3 className="text-xl">{langui.type}</h3>
|
||||
<h3 className="text-xl">{format("type", { count: 1 })}</h3>
|
||||
<div className="flex flex-row flex-wrap">
|
||||
<Chip
|
||||
text={
|
||||
type.data.attributes.titles?.[0]?.title ?? prettySlug(type.data.attributes.slug)
|
||||
}
|
||||
/>
|
||||
<Chip text={type} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categories && categories.data.length > 0 && (
|
||||
{releaseDate && (
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-xl">{langui.categories}</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">
|
||||
{filterHasAttributes(categories.data, ["attributes", "id"] as const).map(
|
||||
(category) => (
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
)
|
||||
)}
|
||||
{categories.map((category) => (
|
||||
<Chip key={category} text={category} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{languageSwitcher}
|
||||
</div>
|
||||
{description && <InsetBox className="mt-8">{<Markdawn text={description} />}</InsetBox>}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// eslint-disable-next-line import/named
|
||||
import Tippy, { TippyProps } from "@tippyjs/react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import type { TippyProps } from "@tippyjs/react";
|
||||
import { cJoin } from "helpers/className";
|
||||
import "tippy.js/animations/scale-subtle.css";
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useCallback } from "react";
|
||||
import { Chip } from "components/Chip";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { getStatusDescription } from "helpers/others";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { ContentStatus, useFormat } from "hooks/useFormat";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -21,7 +21,7 @@ interface Props {
|
|||
translations: {
|
||||
language: string | undefined;
|
||||
definition: string | null | undefined;
|
||||
status: string | undefined;
|
||||
status: ContentStatus | undefined;
|
||||
}[];
|
||||
index: number;
|
||||
categories: string[];
|
||||
|
@ -31,7 +31,7 @@ interface Props {
|
|||
|
||||
const DefinitionCard = ({ source, translations = [], index, categories }: Props): JSX.Element => {
|
||||
const isContentPanelAtLeastMd = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastMd);
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const { format, formatStatusDescription } = useFormat();
|
||||
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||
items: translations,
|
||||
languageExtractor: useCallback((item: Props["translations"][number]) => item.language, []),
|
||||
|
@ -40,7 +40,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props)
|
|||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap place-items-center gap-2">
|
||||
<p className="font-headers text-lg font-bold">{`${langui.definition} ${index}`}</p>
|
||||
<p className="font-headers text-lg font-bold">{format("definition_x", { x: index })}</p>
|
||||
|
||||
{translations.length > 1 && (
|
||||
<>
|
||||
|
@ -53,7 +53,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props)
|
|||
<>
|
||||
<Separator />
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedTranslation.status, langui)}
|
||||
content={formatStatusDescription(selectedTranslation.status)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.status} />
|
||||
</ToolTip>
|
||||
|
@ -80,7 +80,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props)
|
|||
"mt-3 flex place-items-center gap-2",
|
||||
cIf(!isContentPanelAtLeastMd, "flex-col text-center")
|
||||
)}>
|
||||
<p>{langui.source}: </p>
|
||||
<p>{format("source")}: </p>
|
||||
<Button href={source.url} size="small" text={source.name} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -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 { atom } from "jotai";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { LightBox } from "components/LightBox";
|
||||
import { filterDefined } from "helpers/others";
|
||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
||||
|
||||
const lightBoxAtom = atomPairing(
|
||||
atom<{
|
||||
showLightBox: (
|
||||
images: (UploadImageFragment | string | null | undefined)[],
|
||||
index?: number
|
||||
) => void;
|
||||
}>({ showLightBox: () => null })
|
||||
);
|
||||
|
||||
export const lightBox = lightBoxAtom[0];
|
||||
import { filterDefined } from "helpers/asserts";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { internalAtoms } from "contexts/atoms";
|
||||
|
||||
export const LightBoxProvider = (): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
|
||||
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
|
||||
const [lightBoxIndex, setLightBoxIndex] = useState(0);
|
||||
|
||||
const setShowLightBox = useAtomSetter(lightBoxAtom);
|
||||
const setShowLightBox = useAtomSetter(internalAtoms.lightBox);
|
||||
|
||||
useEffectOnce(() =>
|
||||
setShowLightBox({
|
||||
|
@ -40,6 +31,8 @@ export const LightBoxProvider = (): JSX.Element => {
|
|||
setTimeout(() => setLightBoxImages([]), 100);
|
||||
}, []);
|
||||
|
||||
useEffect(() => router.events.on("routeChangeStart", closeLightBox));
|
||||
|
||||
return (
|
||||
<LightBox
|
||||
isVisible={isLightBoxVisible}
|
||||
|
|
|
@ -1,29 +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 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");
|
||||
setSettingsOpened(false);
|
||||
setMainPanelOpened(false);
|
||||
setSubPanelOpened(false);
|
||||
});
|
||||
|
||||
router.events.on("hashChangeStart", () => {
|
||||
console.log("[Router Events] on hashChangeStart");
|
||||
setSubPanelOpened(false);
|
||||
});
|
||||
}, [router, setSettingsOpened, setMainPanelOpened, setSubPanelOpened]);
|
||||
|
||||
useScrollIntoView();
|
||||
};
|
|
@ -1,49 +1,91 @@
|
|||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { localData } from "contexts/localData";
|
||||
import { containerQueries } from "contexts/containerQueries";
|
||||
import { userAgent } from "contexts/userAgent";
|
||||
import { atomPairing } from "helpers/atoms";
|
||||
import { settings } from "contexts/settings";
|
||||
import { lightBox } from "contexts/LightBoxProvider";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { Languages, Currencies, Langui, Recorders, TypesTranslations } from "helpers/localData";
|
||||
|
||||
/*
|
||||
* I'm getting a weird error if I put those atoms in appLayout.ts
|
||||
* So I'm putting the atoms here. Sucks, I know.
|
||||
*/
|
||||
/* [ LOCAL DATA ATOMS ] */
|
||||
|
||||
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 ] */
|
||||
|
||||
const mainPanelReduced = atomPairing(atomWithStorage("isMainPanelReduced", false));
|
||||
const settingsOpened = atomPairing(atomWithStorage("isSettingsOpened", false));
|
||||
const subPanelOpened = atomPairing(atomWithStorage("isSubPanelOpened", false));
|
||||
const mainPanelOpened = atomPairing(atomWithStorage("isMainPanelOpened", false));
|
||||
const searchOpened = atomPairing(atom(false));
|
||||
const settingsOpened = atomPairing(atom(false));
|
||||
const subPanelOpened = atomPairing(atom(false));
|
||||
const mainPanelOpened = atomPairing(atom(false));
|
||||
const debugMenuOpened = atomPairing(atom(false));
|
||||
const debugMenuAvailable = atom((get) => get(settings.playerName[0]) === "debug");
|
||||
const menuGesturesEnabled = atomPairing(atomWithStorage("isMenuGesturesEnabled", false));
|
||||
const terminalMode = atom((get) => get(settings.playerName[0]) === "root");
|
||||
|
||||
const layout = {
|
||||
searchOpened,
|
||||
mainPanelReduced,
|
||||
settingsOpened,
|
||||
subPanelOpened,
|
||||
mainPanelOpened,
|
||||
menuGesturesEnabled,
|
||||
terminalMode,
|
||||
};
|
||||
|
||||
/* [ TERMINAL ATOMS ] */
|
||||
|
||||
const previousLines = atomPairing(atom<string[]>([]));
|
||||
const previousCommands = atomPairing(atom<string[]>([]));
|
||||
|
||||
const terminal = {
|
||||
previousLines,
|
||||
previousCommands,
|
||||
debugMenuAvailable,
|
||||
debugMenuOpened,
|
||||
};
|
||||
|
||||
export const atoms = {
|
||||
settings,
|
||||
layout,
|
||||
terminal,
|
||||
localData,
|
||||
lightBox,
|
||||
containerQueries,
|
||||
userAgent,
|
||||
};
|
||||
|
||||
// Do not import outside of the "contexts" folder
|
||||
export const internalAtoms = {
|
||||
lightBox: lightBoxAtom,
|
||||
localData: { languages, currencies, langui, fallbackLangui, recorders, typesTranslations },
|
||||
};
|
||||
|
|
|
@ -1,39 +1,35 @@
|
|||
import { atom } from "jotai";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useFetch } from "usehooks-ts";
|
||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
||||
import {
|
||||
Languages,
|
||||
Currencies,
|
||||
Langui,
|
||||
processLangui,
|
||||
processCurrencies,
|
||||
processLanguages,
|
||||
} from "helpers/localData";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
|
||||
import {
|
||||
LocalDataGetWebsiteInterfacesQuery,
|
||||
LocalDataGetCurrenciesQuery,
|
||||
LocalDataGetLanguagesQuery,
|
||||
LocalDataGetRecordersQuery,
|
||||
} from "graphql/generated";
|
||||
import { LocalDataFile } from "graphql/fetchLocalData";
|
||||
|
||||
const languages = atomPairing(atom<Languages>([]));
|
||||
const currencies = atomPairing(atom<Currencies>([]));
|
||||
const langui = atomPairing(atom<Langui>({}));
|
||||
|
||||
export const localData = {
|
||||
languages: languages[0],
|
||||
currencies: currencies[0],
|
||||
langui: langui[0],
|
||||
};
|
||||
import { internalAtoms } from "contexts/atoms";
|
||||
import {
|
||||
processLanguages,
|
||||
processCurrencies,
|
||||
processLangui,
|
||||
processRecorders,
|
||||
processTypesTranslations,
|
||||
} from "helpers/localData";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
|
||||
const logger = getLogger("💽 [Local Data]");
|
||||
|
||||
export const useLocalData = (): void => {
|
||||
const setLanguages = useAtomSetter(languages);
|
||||
const setCurrencies = useAtomSetter(currencies);
|
||||
const setLangui = useAtomSetter(langui);
|
||||
const setLanguages = useAtomSetter(internalAtoms.localData.languages);
|
||||
const setCurrencies = useAtomSetter(internalAtoms.localData.currencies);
|
||||
const setLangui = useAtomSetter(internalAtoms.localData.langui);
|
||||
const setFallbackLangui = useAtomSetter(internalAtoms.localData.fallbackLangui);
|
||||
const setRecorders = useAtomSetter(internalAtoms.localData.recorders);
|
||||
const setTypesTranslations = useAtomSetter(internalAtoms.localData.typesTranslations);
|
||||
|
||||
const { locale } = useRouter();
|
||||
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
||||
|
@ -41,19 +37,38 @@ export const useLocalData = (): void => {
|
|||
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
||||
getFileName("websiteInterfaces")
|
||||
);
|
||||
const { data: rawRecorders } = useFetch<LocalDataGetRecordersQuery>(getFileName("recorders"));
|
||||
const { data: rawTypesTranslations } = useFetch<LocalDataGetRecordersQuery>(
|
||||
getFileName("typesTranslations")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[useLocalData] Refresh languages");
|
||||
logger.log("Refresh languages");
|
||||
setLanguages(processLanguages(rawLanguages));
|
||||
}, [rawLanguages, setLanguages]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[useLocalData] Refresh currencies");
|
||||
logger.log("Refresh currencies");
|
||||
setCurrencies(processCurrencies(rawCurrencies));
|
||||
}, [rawCurrencies, setCurrencies]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[useLocalData] Refresh langui");
|
||||
logger.log("Refresh langui");
|
||||
setLangui(processLangui(rawLangui, locale));
|
||||
}, [locale, rawLangui, setLangui]);
|
||||
|
||||
useEffect(() => {
|
||||
logger.log("Refresh fallback langui");
|
||||
setFallbackLangui(processLangui(rawLangui, "en"));
|
||||
}, [rawLangui, setFallbackLangui]);
|
||||
|
||||
useEffect(() => {
|
||||
logger.log("Refresh recorders");
|
||||
setRecorders(processRecorders(rawRecorders));
|
||||
}, [rawRecorders, setRecorders]);
|
||||
|
||||
useEffect(() => {
|
||||
logger.log("Refresh types translations");
|
||||
setTypesTranslations(processTypesTranslations(rawTypesTranslations));
|
||||
}, [rawTypesTranslations, setTypesTranslations]);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useLayoutEffect, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
import { getDefaultPreferredLanguages } from "helpers/locales";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { isDefined } from "helpers/asserts";
|
||||
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
||||
import { userAgent } from "contexts/userAgent";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
export enum ThemeMode {
|
||||
Dark = "dark",
|
||||
|
@ -13,13 +14,45 @@ export enum ThemeMode {
|
|||
Light = "light",
|
||||
}
|
||||
|
||||
export enum PerfMode {
|
||||
On = "on",
|
||||
Auto = "auto",
|
||||
Off = "off",
|
||||
}
|
||||
|
||||
const logger = getLogger("⚙️ [Settings Context]");
|
||||
|
||||
const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", []));
|
||||
const themeModeAtom = atomPairing(atomWithStorage<ThemeMode>("themeMode", ThemeMode.Auto));
|
||||
const themeModeAtom = atomPairing(atomWithStorage("themeMode", ThemeMode.Auto));
|
||||
const darkModeAtom = atomPairing(atom(false));
|
||||
const fontSizeAtom = atomPairing(atomWithStorage("fontSize", 1));
|
||||
const dyslexicAtom = atomPairing(atomWithStorage("isDyslexic", false));
|
||||
const currencyAtom = atomPairing(atomWithStorage("currency", "USD"));
|
||||
const playerNameAtom = atomPairing(atomWithStorage("playerName", ""));
|
||||
const perfModeAtom = atomPairing(atomWithStorage("perfMode", PerfMode.Auto));
|
||||
|
||||
const isPerfModeEnabledAtom = atom((get) => {
|
||||
const os = get(userAgent.os);
|
||||
const engine = get(userAgent.engine);
|
||||
const perfMode = get(perfModeAtom[0]);
|
||||
|
||||
if (os === "iOS") return true;
|
||||
if (engine === "WebKit") return true;
|
||||
if (perfMode === "auto") {
|
||||
if (engine === "Blink") return false;
|
||||
if (os === "Linux") return true;
|
||||
if (os === "Android") return true;
|
||||
}
|
||||
return perfMode === PerfMode.On;
|
||||
});
|
||||
|
||||
const isPerfModeToggleableAtom = atom((get) => {
|
||||
const engine = get(userAgent.engine);
|
||||
const os = get(userAgent.os);
|
||||
if (os === "iOS") return false;
|
||||
if (engine === "WebKit") return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
export const settings = {
|
||||
preferredLanguages: preferredLanguagesAtom,
|
||||
|
@ -29,6 +62,9 @@ export const settings = {
|
|||
dyslexic: dyslexicAtom,
|
||||
currency: currencyAtom,
|
||||
playerName: playerNameAtom,
|
||||
perfMode: perfModeAtom,
|
||||
isPerfModeEnabled: isPerfModeEnabledAtom,
|
||||
isPerfModeToggleable: isPerfModeToggleableAtom,
|
||||
};
|
||||
|
||||
export const useSettings = (): void => {
|
||||
|
@ -39,14 +75,14 @@ export const useSettings = (): void => {
|
|||
const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom);
|
||||
const themeMode = useAtomGetter(themeModeAtom);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const html = document.getElementsByTagName("html")[0];
|
||||
if (isDefined(html)) {
|
||||
html.style.fontSize = `${fontSize * 100}%`;
|
||||
}
|
||||
}, [fontSize]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const next = document.getElementById("__next");
|
||||
if (isDefined(next)) {
|
||||
if (isDyslexic) {
|
||||
|
@ -66,7 +102,7 @@ export const useSettings = (): void => {
|
|||
setDarkMode(themeMode === ThemeMode.Auto ? prefersDarkMode : themeMode === ThemeMode.Dark);
|
||||
}, [prefersDarkMode, setDarkMode, themeMode]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const next = document.getElementById("__next");
|
||||
if (isDefined(next)) {
|
||||
if (isDarkMode) {
|
||||
|
@ -80,24 +116,33 @@ export const useSettings = (): void => {
|
|||
}, [isDarkMode]);
|
||||
|
||||
/* PREFERRED LANGUAGES */
|
||||
|
||||
useEffect(() => {
|
||||
if (preferredLanguages.length === 0) {
|
||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
||||
}
|
||||
} else if (router.locale !== preferredLanguages[0]) {
|
||||
/*
|
||||
* Using a timeout to the code getting stuck into a loop when reaching the website with a
|
||||
* different preferredLanguages[0] from router.locale
|
||||
*/
|
||||
setTimeout(
|
||||
async () =>
|
||||
router.replace(router.asPath, router.asPath, {
|
||||
locale: preferredLanguages[0],
|
||||
}),
|
||||
250
|
||||
if (!router.locale || !router.locales) return;
|
||||
const localStorageValue: string[] = JSON.parse(
|
||||
localStorage.getItem("preferredLanguages") ?? "[]"
|
||||
);
|
||||
|
||||
if (localStorageValue.length === 0) {
|
||||
const defaultLanguages = router.locales;
|
||||
defaultLanguages.sort((a, b) => {
|
||||
const evaluate = (value: string) =>
|
||||
navigator.languages.includes(value)
|
||||
? navigator.languages.findIndex((v) => value === v)
|
||||
: navigator.languages.length;
|
||||
return evaluate(a) - evaluate(b);
|
||||
});
|
||||
logger.log("First time visitor, initializing preferred languages to", defaultLanguages);
|
||||
setPreferredLanguages(defaultLanguages);
|
||||
} else if (router.locale !== localStorageValue[0]) {
|
||||
logger.log(
|
||||
"Router locale",
|
||||
router.locale,
|
||||
"doesn't correspond to preferred locale. Switching to",
|
||||
localStorageValue[0]
|
||||
);
|
||||
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]);
|
||||
};
|
|
@ -3,17 +3,33 @@ import { resolve } from "path";
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { config } from "dotenv";
|
||||
import { getReadySdk } from "./sdk";
|
||||
import { LocalDataGetWebsiteInterfacesQuery } from "./generated";
|
||||
import { processLangui, Langui } from "helpers/localData";
|
||||
import {
|
||||
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") });
|
||||
|
||||
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`;
|
||||
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 => {
|
||||
|
@ -21,19 +37,68 @@ const readLocalData = <T>(name: LocalDataFile): T => {
|
|||
return JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
||||
};
|
||||
|
||||
const sdk = getReadySdk();
|
||||
|
||||
(async () => {
|
||||
export const fetchWebsiteInterfaces = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("websiteInterfaces", await sdk.localDataGetWebsiteInterfaces());
|
||||
};
|
||||
|
||||
export const fetchCurrencies = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("currencies", await sdk.localDataGetCurrencies());
|
||||
};
|
||||
|
||||
export const fetchLanguages = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("languages", await sdk.localDataGetLanguages());
|
||||
})();
|
||||
};
|
||||
|
||||
export const fetchRecorders = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("recorders", await sdk.localDataGetRecorders());
|
||||
};
|
||||
|
||||
export const fetchTypesTranslations = async (): Promise<void> => {
|
||||
const sdk = getReadySdk();
|
||||
writeLocalData("typesTranslations", await sdk.localDataGetTypesTranslations());
|
||||
};
|
||||
|
||||
const fetchLocalData = async (): Promise<void> => {
|
||||
await fetchWebsiteInterfaces();
|
||||
await fetchCurrencies();
|
||||
await fetchLanguages();
|
||||
await fetchRecorders();
|
||||
await fetchTypesTranslations();
|
||||
};
|
||||
|
||||
if (process.argv[2] === "--esrun") {
|
||||
fetchLocalData();
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export type LocalDataFile = "currencies" | "languages" | "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");
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { getReadySdk } from "./sdk";
|
||||
import { getLangui } from "./fetchLocalData";
|
||||
import { PostWithTranslations } from "types/types";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { prettyDate, prettySlug } from "helpers/formatters";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||
import { filterHasAttributes, isDefined } from "helpers/others";
|
||||
import { filterHasAttributes } from "helpers/asserts";
|
||||
import { getDescription } from "helpers/description";
|
||||
import { AppLayoutRequired } from "components/AppLayout";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
|
||||
export interface PostStaticProps extends AppLayoutRequired {
|
||||
post: PostWithTranslations;
|
||||
|
@ -17,47 +17,40 @@ export const getPostStaticProps =
|
|||
(slug: string): GetStaticProps =>
|
||||
async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const langui = getLangui(context.locale);
|
||||
const { format, formatCategory, formatDate } = getFormat(context.locale);
|
||||
const post = await sdk.getPost({
|
||||
slug: slug,
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (
|
||||
post.posts?.data &&
|
||||
post.posts.data.length > 0 &&
|
||||
post.posts.data[0].attributes?.translations &&
|
||||
isDefined(context.locale) &&
|
||||
isDefined(context.locales)
|
||||
) {
|
||||
const selectedTranslation = staticSmartLanguage({
|
||||
items: post.posts.data[0].attributes.translations,
|
||||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
||||
});
|
||||
|
||||
const title = selectedTranslation?.title ?? prettySlug(slug);
|
||||
|
||||
const description = getDescription(selectedTranslation?.excerpt, {
|
||||
[langui.release_date ?? "Release date"]: [
|
||||
prettyDate(post.posts.data[0].attributes.date, context.locale),
|
||||
],
|
||||
[langui.categories ?? "Categories"]: 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(langui, title, description, thumbnail),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
if (!post.posts?.data[0]?.attributes?.translations || !context.locale || !context.locales) {
|
||||
return { notFound: true };
|
||||
}
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
export interface ICUParams {
|
||||
library: never;
|
||||
contents: never;
|
||||
wiki: never;
|
||||
chronicles: never;
|
||||
library_short_description: never;
|
||||
contents_short_description: never;
|
||||
wiki_short_description: never;
|
||||
chronicles_short_description: never;
|
||||
news: never;
|
||||
gallery: never;
|
||||
archives: never;
|
||||
about_us: never;
|
||||
licensing_notice: never;
|
||||
copyright_notice: never;
|
||||
contents_description: never;
|
||||
type: { count: number };
|
||||
category: { count: number };
|
||||
size: never;
|
||||
release_date: never;
|
||||
details: never;
|
||||
price: never;
|
||||
width: never;
|
||||
height: never;
|
||||
thickness: never;
|
||||
subitem: { count: number };
|
||||
variant: { count: number };
|
||||
summary: never;
|
||||
audio: never;
|
||||
video: never;
|
||||
textual: never;
|
||||
game: never;
|
||||
other: never;
|
||||
left_to_right: never;
|
||||
right_to_left: never;
|
||||
page: { count: number };
|
||||
page_order: never;
|
||||
binding: never;
|
||||
type_information: never;
|
||||
front_matter: never;
|
||||
back_matter: never;
|
||||
open_content: never;
|
||||
view_scans: never;
|
||||
paperback: never;
|
||||
hardcover: never;
|
||||
language: { count: number };
|
||||
library_description: never;
|
||||
wiki_description: never;
|
||||
chronicles_description: never;
|
||||
news_description: never;
|
||||
archives_description: never;
|
||||
about_us_description: never;
|
||||
page_not_found: never;
|
||||
default_description: never;
|
||||
name: never;
|
||||
show_subitems: never;
|
||||
show_primary_items: never;
|
||||
show_secondary_items: never;
|
||||
order_by: never;
|
||||
select_option_sidebar: never;
|
||||
group: never;
|
||||
settings: never;
|
||||
theme: never;
|
||||
light: never;
|
||||
auto: never;
|
||||
dark: never;
|
||||
font_size: never;
|
||||
player_name: never;
|
||||
currency: never;
|
||||
font: never;
|
||||
calculated: never;
|
||||
status_incomplete: never;
|
||||
status_draft: never;
|
||||
status_review: never;
|
||||
status_done: never;
|
||||
incomplete: never;
|
||||
draft: never;
|
||||
review: never;
|
||||
done: never;
|
||||
status: never;
|
||||
transcript_notice: never;
|
||||
translation_notice: never;
|
||||
source_language: never;
|
||||
pronouns: never;
|
||||
item: { count: number };
|
||||
open_settings: never;
|
||||
open_search: never;
|
||||
chronology: never;
|
||||
accords_handbook: never;
|
||||
legality: never;
|
||||
sharing_policy: never;
|
||||
contact_us: never;
|
||||
email: never;
|
||||
email_gdpr_notice: never;
|
||||
message: never;
|
||||
send: never;
|
||||
response_invalid_code: never;
|
||||
response_invalid_email: never;
|
||||
response_email_success: never;
|
||||
always_show_info: never;
|
||||
item_not_available: never;
|
||||
primary_language: never;
|
||||
secondary_language: never;
|
||||
previous_content: { count: number };
|
||||
followup_content: { count: number };
|
||||
videos: never;
|
||||
view_on_x: { x: Date | boolean | number | string };
|
||||
channel: never;
|
||||
subscribers: never;
|
||||
description: never;
|
||||
available_at_x: { x: Date | boolean | number | string };
|
||||
want_it: never;
|
||||
have_it: never;
|
||||
source: never;
|
||||
reset_all_filters: never;
|
||||
only_display_items_i_have: never;
|
||||
only_display_items_i_want: never;
|
||||
only_display_unmarked_items: never;
|
||||
table_of_contents: never;
|
||||
no_results_message: never;
|
||||
all: never;
|
||||
special_pages: never;
|
||||
scan: never;
|
||||
scanlation: never;
|
||||
scanners: never;
|
||||
cleaners: never;
|
||||
typesetters: never;
|
||||
notes: never;
|
||||
tags: never;
|
||||
no_source_warning: never;
|
||||
copy_anchor_link: never;
|
||||
anchor_link_copied: never;
|
||||
folders: never;
|
||||
empty_folder_message: never;
|
||||
switch_to_grid_view: never;
|
||||
switch_to_folder_view: never;
|
||||
paper_texture: never;
|
||||
book_fold: never;
|
||||
lighting: never;
|
||||
side_pages: never;
|
||||
shadow: never;
|
||||
night_reader: never;
|
||||
single_page_view: never;
|
||||
double_page_view: never;
|
||||
reset_all_options: never;
|
||||
reading_layout: never;
|
||||
quality: never;
|
||||
only_unavailable_videos: never;
|
||||
oldest: never;
|
||||
newest: never;
|
||||
least_popular: never;
|
||||
most_popular: never;
|
||||
shortest: never;
|
||||
longest: never;
|
||||
search: never;
|
||||
showing_x_out_of_y_results: {
|
||||
x: Date | boolean | number | string;
|
||||
y: Date | boolean | number | string;
|
||||
};
|
||||
return_to_x: { x: undefined | null | Date | boolean | number | string };
|
||||
x_results: { x: number };
|
||||
definition_x: { x: Date | boolean | number | string };
|
||||
subitem_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 };
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/* eslint-disable import/no-nodejs-modules */
|
||||
import { createWriteStream } from "fs";
|
||||
import { parse, TYPE } from "@formatjs/icu-messageformat-parser";
|
||||
import { getLangui } from "./fetchLocalData";
|
||||
import { getLogger } from "helpers/logger";
|
||||
|
||||
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 = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { ui_language, ...langui } = getLangui("en");
|
||||
|
||||
const output = createWriteStream(`${OUTPUT_FOLDER}/icuParams.ts`);
|
||||
|
||||
output.write("export interface ICUParams {\n");
|
||||
|
||||
Object.keys(langui).map((oKey) => {
|
||||
const key = oKey as keyof typeof langui;
|
||||
const parsedMessage = parse(langui[key] ?? "");
|
||||
|
||||
const variables = filterDefined(
|
||||
parsedMessage.map((elem) => {
|
||||
if (elem.type === TYPE.argument) {
|
||||
return `${elem.value}: Date | boolean | number | string`;
|
||||
} else if (elem.type === TYPE.plural) {
|
||||
return `${elem.value}: number`;
|
||||
} else if (elem.type === TYPE.select) {
|
||||
const options = Object.keys(elem.options);
|
||||
const stringOptions = options.filter(
|
||||
(option) => option !== "undefined" && option !== "other"
|
||||
);
|
||||
const type: string[] = stringOptions.map((option) => `"${option}"`);
|
||||
if (options.includes("undefined")) type.push(...["undefined", "null"]);
|
||||
if (options.includes("other")) type.push(...["Date", "boolean", "number", "string"]);
|
||||
return `${elem.value}: ${type.join(` | `)}`;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
|
||||
const variablesType = variables.length > 0 ? `{ ${variables.join(";")} }` : "never";
|
||||
|
||||
output.write(` ${key}: ${variablesType};\n`);
|
||||
});
|
||||
|
||||
output.write("}\n");
|
||||
|
||||
logger.log(`icu-params.ts has been written!`);
|
||||
};
|
||||
|
||||
if (process.argv[2] === "--icu") {
|
||||
icuToTypescript();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
query getChronicle($slug: String, $language_code: String) {
|
||||
query getChronicle($slug: String) {
|
||||
chronicles(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
attributes {
|
||||
|
@ -53,21 +53,21 @@ query getChronicle($slug: String, $language_code: String) {
|
|||
authors {
|
||||
data {
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
translators {
|
||||
data {
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
proofreaders {
|
||||
data {
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,10 +80,8 @@ query getChronicle($slug: String, $language_code: String) {
|
|||
slug
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,9 +89,6 @@ query getChronicle($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +118,7 @@ query getChronicle($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +126,7 @@ query getChronicle($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +134,7 @@ query getChronicle($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query getContentText($slug: String, $language_code: String) {
|
||||
query getContentText($slug: String) {
|
||||
contents(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -6,10 +6,8 @@ query getContentText($slug: String, $language_code: String) {
|
|||
slug
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +15,6 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,10 +48,8 @@ query getContentText($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,19 +60,15 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platforms(pagination: { limit: -1 }) {
|
||||
platform {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,9 +78,6 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,9 +87,6 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,9 +96,6 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,9 +103,6 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +140,7 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +148,7 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +156,44 @@ query getContentText($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
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
|
||||
}
|
||||
sequence
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
translations(pagination: { limit: -1 }) {
|
||||
pre_title
|
||||
title
|
||||
subtitle
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
type {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
previous_contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
...relatedContentPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
next_contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
...relatedContentPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
query getContents($language_code: String) {
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
translations(pagination: { limit: -1 }) {
|
||||
pre_title
|
||||
title
|
||||
subtitle
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
type {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ranged_contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
scan_set {
|
||||
id
|
||||
}
|
||||
library_item {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
title
|
||||
subtitle
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +1,8 @@
|
|||
query getContentsFolder($slug: String, $language_code: String) {
|
||||
query getContentsFolder($slug: String) {
|
||||
contentsFolders(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
...parentFolderPreview
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
|
@ -51,10 +22,8 @@ query getContentsFolder($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,9 +31,6 @@ query getContentsFolder($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
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 } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -9,6 +9,7 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
root_item
|
||||
primary
|
||||
digital
|
||||
download_available
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
|
@ -32,10 +33,8 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,9 +56,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,19 +76,15 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platforms(pagination: { limit: -1 }) {
|
||||
platform {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,21 +118,20 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tracks(pagination: { limit: -1 }) {
|
||||
id
|
||||
slug
|
||||
title
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGroup {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,9 +139,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,10 +176,8 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,19 +188,16 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platforms {
|
||||
platform {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,9 +207,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,9 +216,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -248,9 +225,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -258,9 +232,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,10 +282,8 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
slug
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -322,9 +291,6 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
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 } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -6,6 +6,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
slug
|
||||
title
|
||||
subtitle
|
||||
download_available
|
||||
images(pagination: { limit: -1 }) {
|
||||
status
|
||||
language {
|
||||
|
@ -26,7 +27,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +35,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +43,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,10 +156,8 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,19 +169,16 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platforms {
|
||||
platform {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,9 +188,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,9 +197,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,9 +206,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -226,9 +213,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +273,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +281,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +289,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
query getLibraryItemsPreview($language_code: String) {
|
||||
libraryItems(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
title
|
||||
subtitle
|
||||
slug
|
||||
root_item
|
||||
primary
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
release_date {
|
||||
...datePicker
|
||||
}
|
||||
price {
|
||||
...pricePicker
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata {
|
||||
__typename
|
||||
... on ComponentMetadataBooks {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platforms(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataVideo {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataAudio {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGroup {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subitems_type {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
query getPost($slug: String, $language_code: String) {
|
||||
query getPost($slug: String) {
|
||||
posts(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -12,16 +12,14 @@ query getPost($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
query getPostsPreview {
|
||||
posts(filters: { hidden: { eq: false } }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
date {
|
||||
...datePicker
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
translations(pagination: { limit: -1 }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
excerpt
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,9 +23,8 @@ query getVideo($uid: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,29 +5,6 @@ query getVideoChannel($channel: String) {
|
|||
uid
|
||||
title
|
||||
subscribers
|
||||
videos(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
uid
|
||||
title
|
||||
views
|
||||
duration
|
||||
gone
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
published_date {
|
||||
...datePicker
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
query getVideosPreview {
|
||||
videos(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
uid
|
||||
title
|
||||
views
|
||||
duration
|
||||
gone
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
published_date {
|
||||
...datePicker
|
||||
}
|
||||
channel {
|
||||
data {
|
||||
attributes {
|
||||
uid
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 } }) {
|
||||
data {
|
||||
id
|
||||
|
@ -13,21 +13,15 @@ query getWikiPage($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
tags {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +52,7 @@ query getWikiPage($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +60,7 @@ query getWikiPage($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +68,7 @@ query getWikiPage($slug: String, $language_code: String) {
|
|||
data {
|
||||
id
|
||||
attributes {
|
||||
...recorderChip
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,10 +84,8 @@ query getWikiPage($slug: String, $language_code: String) {
|
|||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
query getWikiPagesPreviews($language_code: String) {
|
||||
wikiPages(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
tags(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: $language_code } } }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
translations(pagination: { limit: -1 }) {
|
||||
title
|
||||
aliases(pagination: { limit: -1 }) {
|
||||
alias
|
||||
}
|
||||
summary
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue