Continue to improve code with hooks
This commit is contained in:
		
							parent
							
								
									d0b91f9db6
								
							
						
					
					
						commit
						2443dee83f
					
				
							
								
								
									
										135
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										135
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -18,8 +18,8 @@ | ||||
|         "meilisearch": "^0.25.1", | ||||
|         "next": "^12.1.6", | ||||
|         "nodemailer": "^6.7.5", | ||||
|         "react": "18.2.0", | ||||
|         "react-dom": "18.2.0", | ||||
|         "react": "18.1.0", | ||||
|         "react-dom": "18.1.0", | ||||
|         "react-hot-keys": "^2.7.2", | ||||
|         "react-swipeable": "^7.0.0", | ||||
|         "turndown": "^7.1.1" | ||||
| @ -29,22 +29,23 @@ | ||||
|         "@graphql-codegen/typescript": "2.5.1", | ||||
|         "@graphql-codegen/typescript-graphql-request": "^4.4.10", | ||||
|         "@graphql-codegen/typescript-operations": "^2.4.2", | ||||
|         "@types/node": "18.0.0", | ||||
|         "@types/node": "17.0.41", | ||||
|         "@types/nodemailer": "^6.4.4", | ||||
|         "@types/react": "18.0.14", | ||||
|         "@types/react": "18.0.12", | ||||
|         "@types/react-dom": "^18.0.5", | ||||
|         "@types/turndown": "^5.0.1", | ||||
|         "@typescript-eslint/eslint-plugin": "^5.28.0", | ||||
|         "@typescript-eslint/parser": "^5.28.0", | ||||
|         "eslint": "^8.18.0", | ||||
|         "@typescript-eslint/eslint-plugin": "^5.27.1", | ||||
|         "@typescript-eslint/parser": "^5.27.1", | ||||
|         "eslint": "^8.17.0", | ||||
|         "eslint-config-next": "12.1.6", | ||||
|         "graphql": "^16.5.0", | ||||
|         "next-sitemap": "^3.0.5", | ||||
|         "prettier": "^2.7.1", | ||||
|         "prettier-plugin-organize-imports": "^3.0.0", | ||||
|         "prettier": "^2.6.2", | ||||
|         "prettier-plugin-organize-imports": "^2.3.4", | ||||
|         "prettier-plugin-tailwindcss": "^0.1.11", | ||||
|         "tailwindcss": "^3.1.3", | ||||
|         "typescript": "^4.7.4" | ||||
|         "tailwindcss": "^3.1.2", | ||||
|         "ts-unused-exports": "^8.0.0", | ||||
|         "typescript": "^4.7.3" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@ampproject/remapping": { | ||||
| @ -2604,9 +2605,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "18.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", | ||||
|       "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", | ||||
|       "version": "17.0.41", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", | ||||
|       "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@types/nodemailer": { | ||||
| @ -2631,9 +2632,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@types/react": { | ||||
|       "version": "18.0.14", | ||||
|       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", | ||||
|       "integrity": "sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==", | ||||
|       "version": "18.0.12", | ||||
|       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.12.tgz", | ||||
|       "integrity": "sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@types/prop-types": "*", | ||||
| @ -7587,9 +7588,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/prettier-plugin-organize-imports": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.0.0.tgz", | ||||
|       "integrity": "sha512-juSCJs5TMOqGGPaN/A/1xzWFzRPH2LG1LPLCr64dzKaVnPafJdtgDNmDVlU+8A4LbQzVJg0DTvgA8swBuIUhlg==", | ||||
|       "version": "2.3.4", | ||||
|       "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-2.3.4.tgz", | ||||
|       "integrity": "sha512-R8o23sf5iVL/U71h9SFUdhdOEPsi3nm42FD/oDYIZ2PQa4TNWWuWecxln6jlIQzpZTDMUeO1NicJP6lLn2TtRw==", | ||||
|       "dev": true, | ||||
|       "peerDependencies": { | ||||
|         "prettier": ">=2.0", | ||||
| @ -7703,9 +7704,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react": { | ||||
|       "version": "18.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", | ||||
|       "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", | ||||
|       "version": "18.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", | ||||
|       "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", | ||||
|       "dependencies": { | ||||
|         "loose-envify": "^1.1.0" | ||||
|       }, | ||||
| @ -7714,15 +7715,15 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react-dom": { | ||||
|       "version": "18.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", | ||||
|       "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", | ||||
|       "version": "18.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", | ||||
|       "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", | ||||
|       "dependencies": { | ||||
|         "loose-envify": "^1.1.0", | ||||
|         "scheduler": "^0.23.0" | ||||
|         "scheduler": "^0.22.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "react": "^18.2.0" | ||||
|         "react": "^18.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react-hot-keys": { | ||||
| @ -8045,9 +8046,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/scheduler": { | ||||
|       "version": "0.23.0", | ||||
|       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", | ||||
|       "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", | ||||
|       "version": "0.22.0", | ||||
|       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", | ||||
|       "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", | ||||
|       "dependencies": { | ||||
|         "loose-envify": "^1.1.0" | ||||
|       } | ||||
| @ -8525,6 +8526,30 @@ | ||||
|       "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/ts-unused-exports": { | ||||
|       "version": "8.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-8.0.0.tgz", | ||||
|       "integrity": "sha512-gylHFyJqC80PSb4zy35KTckykEW1vmKjnOHjBeX9iKBo4b/SzqQIcXXbYSuif4YMgNm6ewFF62VM1C9z0bGZPw==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "chalk": "^4.0.0", | ||||
|         "tsconfig-paths": "^3.9.0" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "ts-unused-exports": "bin/ts-unused-exports" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/pzavolinsky/ts-unused-exports?sponsor=1" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "typescript": ">=3.8.3" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "typescript": { | ||||
|           "optional": false | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/tsconfig-paths": { | ||||
|       "version": "3.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", | ||||
| @ -11003,9 +11028,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@types/node": { | ||||
|       "version": "18.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", | ||||
|       "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", | ||||
|       "version": "17.0.41", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", | ||||
|       "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@types/nodemailer": { | ||||
| @ -11030,9 +11055,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@types/react": { | ||||
|       "version": "18.0.14", | ||||
|       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", | ||||
|       "integrity": "sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==", | ||||
|       "version": "18.0.12", | ||||
|       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.12.tgz", | ||||
|       "integrity": "sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@types/prop-types": "*", | ||||
| @ -14738,9 +14763,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "prettier-plugin-organize-imports": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.0.0.tgz", | ||||
|       "integrity": "sha512-juSCJs5TMOqGGPaN/A/1xzWFzRPH2LG1LPLCr64dzKaVnPafJdtgDNmDVlU+8A4LbQzVJg0DTvgA8swBuIUhlg==", | ||||
|       "version": "2.3.4", | ||||
|       "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-2.3.4.tgz", | ||||
|       "integrity": "sha512-R8o23sf5iVL/U71h9SFUdhdOEPsi3nm42FD/oDYIZ2PQa4TNWWuWecxln6jlIQzpZTDMUeO1NicJP6lLn2TtRw==", | ||||
|       "dev": true, | ||||
|       "requires": {} | ||||
|     }, | ||||
| @ -14819,20 +14844,20 @@ | ||||
|       } | ||||
|     }, | ||||
|     "react": { | ||||
|       "version": "18.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", | ||||
|       "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", | ||||
|       "version": "18.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", | ||||
|       "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", | ||||
|       "requires": { | ||||
|         "loose-envify": "^1.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "react-dom": { | ||||
|       "version": "18.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", | ||||
|       "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", | ||||
|       "version": "18.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", | ||||
|       "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", | ||||
|       "requires": { | ||||
|         "loose-envify": "^1.1.0", | ||||
|         "scheduler": "^0.23.0" | ||||
|         "scheduler": "^0.22.0" | ||||
|       } | ||||
|     }, | ||||
|     "react-hot-keys": { | ||||
| @ -15059,9 +15084,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "scheduler": { | ||||
|       "version": "0.23.0", | ||||
|       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", | ||||
|       "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", | ||||
|       "version": "0.22.0", | ||||
|       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", | ||||
|       "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", | ||||
|       "requires": { | ||||
|         "loose-envify": "^1.1.0" | ||||
|       } | ||||
| @ -15428,6 +15453,16 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "ts-unused-exports": { | ||||
|       "version": "8.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-8.0.0.tgz", | ||||
|       "integrity": "sha512-gylHFyJqC80PSb4zy35KTckykEW1vmKjnOHjBeX9iKBo4b/SzqQIcXXbYSuif4YMgNm6ewFF62VM1C9z0bGZPw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "chalk": "^4.0.0", | ||||
|         "tsconfig-paths": "^3.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "tsconfig-paths": { | ||||
|       "version": "3.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", | ||||
|  | ||||
							
								
								
									
										25
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								package.json
									
									
									
									
									
								
							| @ -3,6 +3,8 @@ | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "next dev -p 12499", | ||||
|     "precommit": "npm run prettier && npm run unused-exports && npm run lint && npm run tsc && npm run generate && echo ALL PRECOMMIT CHECKS PASSED SUCCESSFULLY, LET\\'S FUCKING GO!", | ||||
|     "unused-exports": "ts-unused-exports ./tsconfig.json --excludePathsFromReport=src/pages --ignoreFiles=generated", | ||||
|     "prebuild": "npm run generate", | ||||
|     "build": "next build", | ||||
|     "postbuild": "next-sitemap --config next-sitemap.config.js", | ||||
| @ -25,8 +27,8 @@ | ||||
|     "meilisearch": "^0.25.1", | ||||
|     "next": "^12.1.6", | ||||
|     "nodemailer": "^6.7.5", | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
|     "react": "18.1.0", | ||||
|     "react-dom": "18.1.0", | ||||
|     "react-hot-keys": "^2.7.2", | ||||
|     "react-swipeable": "^7.0.0", | ||||
|     "turndown": "^7.1.1" | ||||
| @ -36,21 +38,22 @@ | ||||
|     "@graphql-codegen/typescript": "2.5.1", | ||||
|     "@graphql-codegen/typescript-graphql-request": "^4.4.10", | ||||
|     "@graphql-codegen/typescript-operations": "^2.4.2", | ||||
|     "@types/node": "18.0.0", | ||||
|     "@types/node": "17.0.41", | ||||
|     "@types/nodemailer": "^6.4.4", | ||||
|     "@types/react": "18.0.14", | ||||
|     "@types/react": "18.0.12", | ||||
|     "@types/react-dom": "^18.0.5", | ||||
|     "@types/turndown": "^5.0.1", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.28.0", | ||||
|     "@typescript-eslint/parser": "^5.28.0", | ||||
|     "eslint": "^8.18.0", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.27.1", | ||||
|     "@typescript-eslint/parser": "^5.27.1", | ||||
|     "eslint": "^8.17.0", | ||||
|     "eslint-config-next": "12.1.6", | ||||
|     "graphql": "^16.5.0", | ||||
|     "next-sitemap": "^3.0.5", | ||||
|     "prettier": "^2.7.1", | ||||
|     "prettier-plugin-organize-imports": "^3.0.0", | ||||
|     "prettier": "^2.6.2", | ||||
|     "prettier-plugin-organize-imports": "^2.3.4", | ||||
|     "prettier-plugin-tailwindcss": "^0.1.11", | ||||
|     "tailwindcss": "^3.1.3", | ||||
|     "typescript": "^4.7.4" | ||||
|     "tailwindcss": "^3.1.2", | ||||
|     "ts-unused-exports": "^8.0.0", | ||||
|     "typescript": "^4.7.3" | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -12,8 +12,6 @@ import { | ||||
|   isUndefined, | ||||
|   iterateMap, | ||||
| } from "helpers/others"; | ||||
| // import { getClient, Indexes, search, SearchResult } from "helpers/search";
 | ||||
| 
 | ||||
| import { useMediaMobile } from "hooks/useMediaQuery"; | ||||
| import { AnchorIds } from "hooks/useScrollTopOnChange"; | ||||
| import Head from "next/head"; | ||||
| @ -415,31 +413,33 @@ export function AppLayout(props: Props): JSX.Element { | ||||
|             <div className="grid place-items-center gap-8 text-center desktop:grid-cols-2"> | ||||
|               <div> | ||||
|                 <h3 className="text-xl">{langui.theme}</h3> | ||||
|                 <ButtonGroup> | ||||
|                   <Button | ||||
|                     onClick={() => { | ||||
|                       setDarkMode(false); | ||||
|                       setSelectedThemeMode(true); | ||||
|                     }} | ||||
|                     active={selectedThemeMode === true && darkMode === false} | ||||
|                     text={langui.light} | ||||
|                   /> | ||||
|                   <Button | ||||
|                     onClick={() => { | ||||
|                       setSelectedThemeMode(false); | ||||
|                     }} | ||||
|                     active={selectedThemeMode === false} | ||||
|                     text={langui.auto} | ||||
|                   /> | ||||
|                   <Button | ||||
|                     onClick={() => { | ||||
|                       setDarkMode(true); | ||||
|                       setSelectedThemeMode(true); | ||||
|                     }} | ||||
|                     active={selectedThemeMode === true && darkMode === true} | ||||
|                     text={langui.dark} | ||||
|                   /> | ||||
|                 </ButtonGroup> | ||||
|                 <ButtonGroup | ||||
|                   buttonsProps={[ | ||||
|                     { | ||||
|                       onClick: () => { | ||||
|                         setDarkMode(false); | ||||
|                         setSelectedThemeMode(true); | ||||
|                       }, | ||||
|                       active: selectedThemeMode === true && darkMode === false, | ||||
|                       text: langui.light, | ||||
|                     }, | ||||
|                     { | ||||
|                       onClick: () => { | ||||
|                         setSelectedThemeMode(false); | ||||
|                       }, | ||||
|                       active: selectedThemeMode === false, | ||||
|                       text: langui.auto, | ||||
|                     }, | ||||
|                     { | ||||
|                       onClick: () => { | ||||
|                         setDarkMode(true); | ||||
|                         setSelectedThemeMode(true); | ||||
|                       }, | ||||
|                       active: selectedThemeMode === true && darkMode === true, | ||||
|                       text: langui.dark, | ||||
|                     }, | ||||
|                   ]} | ||||
|                 /> | ||||
|               </div> | ||||
| 
 | ||||
|               <div> | ||||
| @ -456,22 +456,27 @@ export function AppLayout(props: Props): JSX.Element { | ||||
| 
 | ||||
|               <div> | ||||
|                 <h3 className="text-xl">{langui.font_size}</h3> | ||||
|                 <ButtonGroup> | ||||
|                   <Button | ||||
|                     onClick={() => setFontSize((fontSize ?? 1) / 1.05)} | ||||
|                     icon={Icon.TextDecrease} | ||||
|                   /> | ||||
|                   <Button | ||||
|                     onClick={() => setFontSize(1)} | ||||
|                     text={`${((fontSize ?? 1) * 100).toLocaleString(undefined, { | ||||
|                       maximumFractionDigits: 0, | ||||
|                     })}%`}
 | ||||
|                   /> | ||||
|                   <Button | ||||
|                     onClick={() => setFontSize((fontSize ?? 1) * 1.05)} | ||||
|                     icon={Icon.TextIncrease} | ||||
|                   /> | ||||
|                 </ButtonGroup> | ||||
|                 <ButtonGroup | ||||
|                   buttonsProps={[ | ||||
|                     { | ||||
|                       onClick: () => setFontSize((fontSize ?? 1) / 1.05), | ||||
|                       icon: Icon.TextDecrease, | ||||
|                     }, | ||||
|                     { | ||||
|                       onClick: () => setFontSize(1), | ||||
|                       text: `${((fontSize ?? 1) * 100).toLocaleString( | ||||
|                         undefined, | ||||
|                         { | ||||
|                           maximumFractionDigits: 0, | ||||
|                         } | ||||
|                       )}%`,
 | ||||
|                     }, | ||||
|                     { | ||||
|                       onClick: () => setFontSize((fontSize ?? 1) * 1.05), | ||||
|                       icon: Icon.TextIncrease, | ||||
|                     }, | ||||
|                   ]} | ||||
|                 /> | ||||
|               </div> | ||||
| 
 | ||||
|               <div> | ||||
| @ -504,61 +509,7 @@ export function AppLayout(props: Props): JSX.Element { | ||||
|             </div> | ||||
|           </div> | ||||
|         </Popup> | ||||
| 
 | ||||
|         {/* <Popup | ||||
|           state={searchPanelOpen} | ||||
|           setState={setSearchPanelOpen} | ||||
|         > | ||||
|           <div className="grid place-items-center gap-2"> | ||||
|             TODO: add to langui | ||||
|             <h2 className="text-2xl">{"Search"}</h2> | ||||
|             <TextInput | ||||
|               className="mb-6 w-full" | ||||
|               placeholder={"Search query..."} | ||||
|               state={searchQuery} | ||||
|               setState={setSearchQuery} | ||||
|             /> | ||||
|           </div> | ||||
|           TODO: add to langui | ||||
|           <div className="grid gap-4"> | ||||
|             <p className="font-headers text-xl">In news:</p> | ||||
|             <div | ||||
|               className="grid grid-cols-2 items-end gap-8 | ||||
|                 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4" | ||||
|             > | ||||
|               {searchResult?.hits.map((hit) => ( | ||||
|                 <PreviewCard | ||||
|                   key={hit.id} | ||||
|                   href={hit.href} | ||||
|                   title={hit.title} | ||||
|                   thumbnailAspectRatio={"3/2"} | ||||
|                   thumbnail={hit.thumbnail} | ||||
|                   keepInfoVisible | ||||
|                 /> | ||||
|               ))} | ||||
|             </div> | ||||
|           </div> | ||||
|         </Popup> */} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * const [searchQuery, setSearchQuery] = useState(""); | ||||
|  * const [searchResult, setSearchResult] = useState<SearchResult>(); | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  * const client = getClient(); | ||||
|  * useEffect(() => { | ||||
|  *   if (searchQuery.length > 1) { | ||||
|  *     search(client, Indexes.Post, searchQuery).then((result) => { | ||||
|  *       setSearchResult(result); | ||||
|  *     }); | ||||
|  *   } else { | ||||
|  *     setSearchResult(undefined); | ||||
|  *   } | ||||
|  *   // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|  * }, [searchQuery]); | ||||
|  */ | ||||
|  | ||||
| @ -58,7 +58,7 @@ export function Button(props: Props): JSX.Element { | ||||
|           id={id} | ||||
|           onClick={onClick} | ||||
|           className={cJoin( | ||||
|             `component-button group grid select-none grid-flow-col place-content-center
 | ||||
|             `group grid select-none grid-flow-col place-content-center
 | ||||
|             place-items-center gap-2 rounded-full border-[1px] border-dark py-3 px-4 leading-none | ||||
|             text-dark transition-all`,
 | ||||
|             cIf( | ||||
|  | ||||
| @ -1,39 +1,53 @@ | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { cJoin } from "helpers/className"; | ||||
| 
 | ||||
| import { useLayoutEffect, useRef } from "react"; | ||||
| import { ConditionalWrapper, Wrapper } from "helpers/component"; | ||||
| import { isDefinedAndNotEmpty } from "helpers/others"; | ||||
| import { Button } from "./Button"; | ||||
| 
 | ||||
| interface Props { | ||||
|   children: React.ReactNode; | ||||
|   className?: string; | ||||
|   buttonsProps: (Parameters<typeof Button>[0] & { | ||||
|     tooltip?: string | null | undefined; | ||||
|   })[]; | ||||
| } | ||||
| 
 | ||||
| export function ButtonGroup(props: Props): JSX.Element { | ||||
|   const { children, className } = props; | ||||
|   const ref = useRef<HTMLDivElement>(null); | ||||
| 
 | ||||
|   useLayoutEffect(() => { | ||||
|     if (ref.current) { | ||||
|       const buttons = ref.current.querySelectorAll(".component-button"); | ||||
|       buttons.forEach((button, index) => { | ||||
|         button.classList.remove("rounded-full"); | ||||
|         button.classList.remove("border-[1px]"); | ||||
|         if (index === 0) { | ||||
|           button.classList.add("rounded-l-full"); | ||||
|           button.classList.add("border-l-[1px]"); | ||||
|         } else if (index === buttons.length - 1) { | ||||
|           button.classList.add("rounded-r-full"); | ||||
|           button.classList.add("border-r-[1px]"); | ||||
|         } else { | ||||
|           button.classList.add("rounded-none"); | ||||
|         } | ||||
|         button.classList.add("border-y-[1px]"); | ||||
|       }); | ||||
|     } | ||||
|   }, [children]); | ||||
|   const { buttonsProps, className } = props; | ||||
| 
 | ||||
|   return ( | ||||
|     <div ref={ref} className={cJoin("grid grid-flow-col", className)}> | ||||
|       {children} | ||||
|     <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> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| interface ToolTipWrapperProps { | ||||
|   text: string; | ||||
| } | ||||
| 
 | ||||
| function ToolTipWrapper(props: ToolTipWrapperProps & Wrapper) { | ||||
|   const { text, children } = props; | ||||
|   return ( | ||||
|     <ToolTip content={text}> | ||||
|       <>{children}</> | ||||
|     </ToolTip> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -1,119 +0,0 @@ | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { GetLibraryItemQuery } from "graphql/generated"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| import { prettyinlineTitle, prettySlug } from "helpers/formatters"; | ||||
| import { filterHasAttributes } from "helpers/others"; | ||||
| 
 | ||||
| import { useToggle } from "hooks/useToggle"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   content: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["contents"] | ||||
|   >["data"][number]; | ||||
|   parentSlug: string; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export function ContentLine(props: Props): JSX.Element { | ||||
|   const { content, langui, parentSlug } = props; | ||||
| 
 | ||||
|   const [opened, setOpened] = useState(false); | ||||
|   const toggleOpened = useToggle(setOpened); | ||||
| 
 | ||||
|   if (content.attributes) { | ||||
|     return ( | ||||
|       <div | ||||
|         className={cJoin( | ||||
|           "grid gap-2 rounded-lg px-4", | ||||
|           cIf(opened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade") | ||||
|         )} | ||||
|       > | ||||
|         <div | ||||
|           className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center | ||||
|         gap-4 thin:grid-cols-[auto_auto_1fr_auto]" | ||||
|         > | ||||
|           <a> | ||||
|             <h3 className="cursor-pointer" onClick={toggleOpened}> | ||||
|               {content.attributes.content?.data?.attributes?.translations?.[0] | ||||
|                 ? prettyinlineTitle( | ||||
|                     content.attributes.content.data.attributes.translations[0] | ||||
|                       ?.pre_title, | ||||
|                     content.attributes.content.data.attributes.translations[0] | ||||
|                       ?.title, | ||||
|                     content.attributes.content.data.attributes.translations[0] | ||||
|                       ?.subtitle | ||||
|                   ) | ||||
|                 : prettySlug(content.attributes.slug, props.parentSlug)} | ||||
|             </h3> | ||||
|           </a> | ||||
|           <div className="flex flex-row flex-wrap gap-1"> | ||||
|             {filterHasAttributes( | ||||
|               content.attributes.content?.data?.attributes?.categories?.data | ||||
|             ).map((category) => ( | ||||
|               <Chip key={category.id}>{category.attributes.short}</Chip> | ||||
|             ))} | ||||
|           </div> | ||||
|           <p className="h-4 w-full border-b-2 border-dotted border-black opacity-30"></p> | ||||
|           <p> | ||||
|             {content.attributes.range[0]?.__typename === | ||||
|             "ComponentRangePageRange" | ||||
|               ? content.attributes.range[0].starting_page | ||||
|               : ""} | ||||
|           </p> | ||||
|           {content.attributes.content?.data?.attributes?.type?.data | ||||
|             ?.attributes && ( | ||||
|             <Chip className="justify-self-end thin:hidden"> | ||||
|               {content.attributes.content.data.attributes.type.data.attributes | ||||
|                 .titles && | ||||
|               content.attributes.content.data.attributes.type.data.attributes | ||||
|                 .titles.length > 0 | ||||
|                 ? content.attributes.content.data.attributes.type.data | ||||
|                     .attributes.titles[0]?.title | ||||
|                 : prettySlug( | ||||
|                     content.attributes.content.data.attributes.type.data | ||||
|                       .attributes.slug | ||||
|                   )} | ||||
|             </Chip> | ||||
|           )} | ||||
|         </div> | ||||
|         <div | ||||
|           className={`grid-flow-col place-content-start place-items-center gap-2 ${ | ||||
|             opened ? "grid" : "hidden" | ||||
|           }`}
 | ||||
|         > | ||||
|           <Ico icon={Icon.SubdirectoryArrowRight} className="text-dark" /> | ||||
| 
 | ||||
|           {content.attributes.scan_set && | ||||
|             content.attributes.scan_set.length > 0 && ( | ||||
|               <Button | ||||
|                 href={`/library/${parentSlug}/scans#${content.attributes.slug}`} | ||||
|                 text={langui.view_scans} | ||||
|               /> | ||||
|             )} | ||||
| 
 | ||||
|           {content.attributes.content?.data && ( | ||||
|             <Button | ||||
|               href={`/contents/${content.attributes.content.data.attributes?.slug}`} | ||||
|               text={langui.open_content} | ||||
|             /> | ||||
|           )} | ||||
| 
 | ||||
|           {content.attributes.scan_set && | ||||
|           content.attributes.scan_set.length === 0 && | ||||
|           !content.attributes.content?.data | ||||
|             ? "The content is not available" | ||||
|             : ""} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|   return <></>; | ||||
| } | ||||
| @ -15,7 +15,7 @@ import { | ||||
| } from "helpers/others"; | ||||
| 
 | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
| @ -49,42 +49,47 @@ export function ScanSet(props: Props): JSX.Element { | ||||
|   const { openLightBox, scanSet, slug, title, languages, langui, content } = | ||||
|     props; | ||||
| 
 | ||||
|   const [selectedScan, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: scanSet, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|     transform: (item) => { | ||||
|       item.pages?.data.sort((a, b) => { | ||||
|         if ( | ||||
|           a.attributes && | ||||
|           b.attributes && | ||||
|           isDefinedAndNotEmpty(a.attributes.url) && | ||||
|           isDefinedAndNotEmpty(b.attributes.url) | ||||
|         ) { | ||||
|           let aName = getAssetFilename(a.attributes.url); | ||||
|           let bName = getAssetFilename(b.attributes.url); | ||||
|   const [selectedScan, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: scanSet, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<Props["scanSet"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|       transform: useCallback((item: NonNullable<Props["scanSet"][number]>) => { | ||||
|         item.pages?.data.sort((a, b) => { | ||||
|           if ( | ||||
|             a.attributes && | ||||
|             b.attributes && | ||||
|             isDefinedAndNotEmpty(a.attributes.url) && | ||||
|             isDefinedAndNotEmpty(b.attributes.url) | ||||
|           ) { | ||||
|             let aName = getAssetFilename(a.attributes.url); | ||||
|             let bName = getAssetFilename(b.attributes.url); | ||||
| 
 | ||||
|           /* | ||||
|            * If the number is a succession of 0s, make the number | ||||
|            * incrementally smaller than 0 (i.e: 00 becomes -1) | ||||
|            */ | ||||
|           if (aName.replaceAll("0", "").length === 0) { | ||||
|             aName = (1 - aName.length).toString(10); | ||||
|           } | ||||
|           if (bName.replaceAll("0", "").length === 0) { | ||||
|             bName = (1 - bName.length).toString(10); | ||||
|           } | ||||
|             /* | ||||
|              * If the number is a succession of 0s, make the number | ||||
|              * incrementally smaller than 0 (i.e: 00 becomes -1) | ||||
|              */ | ||||
|             if (aName.replaceAll("0", "").length === 0) { | ||||
|               aName = (1 - aName.length).toString(10); | ||||
|             } | ||||
|             if (bName.replaceAll("0", "").length === 0) { | ||||
|               bName = (1 - bName.length).toString(10); | ||||
|             } | ||||
| 
 | ||||
|           if (isInteger(aName) && isInteger(bName)) { | ||||
|             return parseInt(aName, 10) - parseInt(bName, 10); | ||||
|             if (isInteger(aName) && isInteger(bName)) { | ||||
|               return parseInt(aName, 10) - parseInt(bName, 10); | ||||
|             } | ||||
|             return a.attributes.url.localeCompare(b.attributes.url); | ||||
|           } | ||||
|           return a.attributes.url.localeCompare(b.attributes.url); | ||||
|         } | ||||
|         return 0; | ||||
|       }); | ||||
|       return item; | ||||
|     }, | ||||
|   }); | ||||
|           return 0; | ||||
|         }); | ||||
|         return item; | ||||
|       }, []), | ||||
|     }); | ||||
| 
 | ||||
|   const pages = useMemo( | ||||
|     () => selectedScan && filterHasAttributes(selectedScan.pages?.data), | ||||
| @ -120,7 +125,7 @@ export function ScanSet(props: Props): JSX.Element { | ||||
|                 /> | ||||
|               )} | ||||
| 
 | ||||
|             <LanguageSwitcher /> | ||||
|             <LanguageSwitcher {...languageSwitcherProps} /> | ||||
| 
 | ||||
|             <div className="grid place-content-center place-items-center"> | ||||
|               <p className="font-headers">{langui.status}:</p> | ||||
|  | ||||
| @ -11,7 +11,7 @@ import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { filterHasAttributes, getStatusDescription } from "helpers/others"; | ||||
| 
 | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
| @ -29,11 +29,16 @@ interface Props { | ||||
| export function ScanSetCover(props: Props): JSX.Element { | ||||
|   const { openLightBox, images, languages, langui } = props; | ||||
| 
 | ||||
|   const [selectedScan, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: images, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
|   const [selectedScan, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: images, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<Props["images"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   const coverImages = useMemo(() => { | ||||
|     const memo: UploadImageFragment[] = []; | ||||
| @ -74,7 +79,7 @@ export function ScanSetCover(props: Props): JSX.Element { | ||||
|             </div> | ||||
| 
 | ||||
|             <div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> | ||||
|               <LanguageSwitcher /> | ||||
|               <LanguageSwitcher {...languageSwitcherProps} /> | ||||
| 
 | ||||
|               <div className="grid place-content-center place-items-center"> | ||||
|                 <p className="font-headers">{langui.status}:</p> | ||||
|  | ||||
| @ -69,7 +69,7 @@ interface TOCInterface { | ||||
|   children: TOCInterface[]; | ||||
| } | ||||
| 
 | ||||
| export function getTocFromMarkdawn(text: string, title?: string): TOCInterface { | ||||
| function getTocFromMarkdawn(text: string, title?: string): TOCInterface { | ||||
|   const toc: TOCInterface = { | ||||
|     title: title ?? "Return to top", | ||||
|     slug: slugify(title), | ||||
|  | ||||
| @ -4,7 +4,7 @@ import { prettySlug } from "helpers/formatters"; | ||||
| import { filterHasAttributes, getStatusDescription } from "helpers/others"; | ||||
| import { PostWithTranslations } from "helpers/types"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| import { AppLayout } from "./AppLayout"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { HorizontalLine } from "./HorizontalLine"; | ||||
| @ -49,11 +49,16 @@ export function PostPage(props: Props): JSX.Element { | ||||
|   } = props; | ||||
|   const displayTitle = props.displayTitle ?? true; | ||||
| 
 | ||||
|   const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: post.translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
|   const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: post.translations, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<PostWithTranslations["translations"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   const { thumbnail, body, title, excerpt } = useMemo( | ||||
|     () => ({ | ||||
| @ -156,7 +161,7 @@ export function PostPage(props: Props): JSX.Element { | ||||
|               description={excerpt} | ||||
|               langui={langui} | ||||
|               categories={post.categories} | ||||
|               languageSwitcher={<LanguageSwitcher />} | ||||
|               languageSwitcher={<LanguageSwitcher {...languageSwitcherProps} />} | ||||
|             /> | ||||
| 
 | ||||
|             <HorizontalLine /> | ||||
| @ -165,7 +170,7 @@ export function PostPage(props: Props): JSX.Element { | ||||
|           <> | ||||
|             {displayLanguageSwitcher && ( | ||||
|               <div className="grid place-content-end place-items-start"> | ||||
|                 <LanguageSwitcher /> | ||||
|                 <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|               </div> | ||||
|             )} | ||||
|             {displayTitle && ( | ||||
| @ -189,6 +194,7 @@ export function PostPage(props: Props): JSX.Element { | ||||
|       displayThumbnailHeader, | ||||
|       displayTitle, | ||||
|       excerpt, | ||||
|       languageSwitcherProps, | ||||
|       langui, | ||||
|       post.categories, | ||||
|       prependBody, | ||||
|  | ||||
| @ -17,6 +17,7 @@ import { ImageQuality } from "helpers/img"; | ||||
| 
 | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import Link from "next/link"; | ||||
| import { useCallback } from "react"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { Ico, Icon } from "./Ico"; | ||||
| import { Img } from "./Img"; | ||||
| @ -290,15 +291,13 @@ export function PreviewCard(props: Props): JSX.Element { | ||||
| 
 | ||||
| interface TranslatedProps | ||||
|   extends Omit<Props, "description" | "pre_title" | "subtitle" | "title"> { | ||||
|   translations: | ||||
|     | { | ||||
|         pre_title?: string | null | undefined; | ||||
|         title: string | null | undefined; | ||||
|         subtitle?: string | null | undefined; | ||||
|         description?: string | null | undefined; | ||||
|         language: string | undefined; | ||||
|       }[] | ||||
|     | undefined; | ||||
|   translations: { | ||||
|     pre_title?: string | null | undefined; | ||||
|     title: string | null | undefined; | ||||
|     subtitle?: string | null | undefined; | ||||
|     description?: string | null | undefined; | ||||
|     language: string | undefined; | ||||
|   }[]; | ||||
|   slug: string; | ||||
|   languages: AppStaticProps["languages"]; | ||||
| } | ||||
| @ -313,7 +312,10 @@ export function TranslatedPreviewCard(props: TranslatedProps): JSX.Element { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: TranslatedProps["translations"][number]) => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { ImageQuality } from "helpers/img"; | ||||
| 
 | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import Link from "next/link"; | ||||
| import { useCallback } from "react"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { Img } from "./Img"; | ||||
| 
 | ||||
| @ -19,7 +20,7 @@ interface Props { | ||||
|   bottomChips?: string[]; | ||||
| } | ||||
| 
 | ||||
| export function PreviewLine(props: Props): JSX.Element { | ||||
| function PreviewLine(props: Props): JSX.Element { | ||||
|   const { | ||||
|     href, | ||||
|     thumbnail, | ||||
| @ -76,14 +77,13 @@ export function PreviewLine(props: Props): JSX.Element { | ||||
| 
 | ||||
| interface TranslatedProps | ||||
|   extends Omit<Props, "pre_title" | "subtitle" | "title"> { | ||||
|   translations: | ||||
|     | { | ||||
|         pre_title?: string | null | undefined; | ||||
|         title: string | null | undefined; | ||||
|         subtitle?: string | null | undefined; | ||||
|         language: string | undefined; | ||||
|       }[] | ||||
|     | undefined; | ||||
|   translations: { | ||||
|     pre_title?: string | null | undefined; | ||||
|     title: string | null | undefined; | ||||
|     subtitle?: string | null | undefined; | ||||
|     language: string | undefined; | ||||
|   }[]; | ||||
| 
 | ||||
|   slug: string; | ||||
|   languages: AppStaticProps["languages"]; | ||||
| } | ||||
| @ -98,7 +98,10 @@ export function TranslatedPreviewLine(props: TranslatedProps): JSX.Element { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: TranslatedProps["translations"][number]) => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -3,16 +3,15 @@ import { ToolTip } from "components/ToolTip"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getStatusDescription } from "helpers/others"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { useCallback } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   source?: string; | ||||
|   translations: | ||||
|     | { | ||||
|         language: string | undefined; | ||||
|         definition: string | null | undefined; | ||||
|         status: string | undefined; | ||||
|       }[] | ||||
|     | undefined; | ||||
|   translations: { | ||||
|     language: string | undefined; | ||||
|     definition: string | null | undefined; | ||||
|     status: string | undefined; | ||||
|   }[]; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   index: number; | ||||
| @ -21,11 +20,15 @@ interface Props { | ||||
| export default function DefinitionCard(props: Props): JSX.Element { | ||||
|   const { source, translations = [], languages, langui, index } = props; | ||||
| 
 | ||||
|   const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language, | ||||
|   }); | ||||
|   const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: translations, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: Props["translations"][number]) => item.language, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
| @ -40,7 +43,9 @@ export default function DefinitionCard(props: Props): JSX.Element { | ||||
|             <Chip>{selectedTranslation.status}</Chip> | ||||
|           </ToolTip> | ||||
|         )} | ||||
|         {translations.length > 1 && <LanguageSwitcher />} | ||||
|         {translations.length > 1 && ( | ||||
|           <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       <p className="italic">{`${langui.source}: ${source}`}</p> | ||||
|  | ||||
| @ -106,8 +106,6 @@ const initialState: RequiredNonNullable<AppLayoutState> = { | ||||
| 
 | ||||
| const AppContext = React.createContext<AppLayoutState>(initialState); | ||||
| 
 | ||||
| export default AppContext; | ||||
| 
 | ||||
| export function useAppLayout(): AppLayoutState { | ||||
|   return useContext(AppContext); | ||||
| } | ||||
|  | ||||
| @ -1,29 +0,0 @@ | ||||
| import { ContentWithTranslations } from "./types"; | ||||
| 
 | ||||
| type Group = NonNullable< | ||||
|   NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable<ContentWithTranslations["group"]>["data"] | ||||
|     >["attributes"] | ||||
|   >["contents"] | ||||
| >["data"]; | ||||
| 
 | ||||
| export function getPreviousContent(group: Group, currentSlug: string) { | ||||
|   for (let index = 0; index < group.length; index += 1) { | ||||
|     const content = group[index]; | ||||
|     if (content.attributes?.slug === currentSlug && index > 0) { | ||||
|       return group[index - 1]; | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| export function getNextContent(group: Group, currentSlug: string) { | ||||
|   for (let index = 0; index < group.length; index += 1) { | ||||
|     const content = group[index]; | ||||
|     if (content.attributes?.slug === currentSlug && index < group.length - 1) { | ||||
|       return group[index + 1]; | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| @ -242,24 +242,12 @@ export function prettyLanguage( | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| export function prettyLanguageToCode( | ||||
|   prettyLanguage: string, | ||||
|   languages: AppStaticProps["languages"] | ||||
| ): string { | ||||
|   let result = prettyLanguage; | ||||
|   languages.forEach((language) => { | ||||
|     if (language?.attributes?.localized_name === prettyLanguage) | ||||
|       result = language.attributes.code; | ||||
|   }); | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| export function prettyURL(url: string): string { | ||||
|   let domain = new URL(url); | ||||
|   return domain.hostname.replace("www.", ""); | ||||
| } | ||||
| 
 | ||||
| export function capitalizeString(string: string): string { | ||||
| function capitalizeString(string: string): string { | ||||
|   function capitalizeWord(word: string): string { | ||||
|     return word.charAt(0).toUpperCase() + word.substring(1); | ||||
|   } | ||||
|  | ||||
| @ -7,7 +7,7 @@ export enum ImageQuality { | ||||
|   Og = "og", | ||||
| } | ||||
| 
 | ||||
| export interface OgImage { | ||||
| interface OgImage { | ||||
|   image: string; | ||||
|   width: number; | ||||
|   height: number; | ||||
| @ -34,7 +34,7 @@ export function getAssetURL(url: string, quality: ImageQuality): string { | ||||
|   return process.env.NEXT_PUBLIC_URL_IMG + newUrl; | ||||
| } | ||||
| 
 | ||||
| export function getImgSizesByMaxSize( | ||||
| function getImgSizesByMaxSize( | ||||
|   width: number, | ||||
|   height: number, | ||||
|   maxSize: number | ||||
|  | ||||
| @ -5,7 +5,9 @@ import { prettyinlineTitle, prettyDate } from "./formatters"; | ||||
| import { convertPrice } from "./numbers"; | ||||
| import { isDefined, mapRemoveEmptyValues } from "./others"; | ||||
| import { LibraryItemUserStatus } from "./types"; | ||||
| type Items = NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; | ||||
| import LibraryPage from "../pages/library/index"; | ||||
| 
 | ||||
| type Items = Parameters<typeof LibraryPage>[0]["items"]; | ||||
| type GroupLibraryItems = Map<string, Items>; | ||||
| 
 | ||||
| export function getGroups( | ||||
|  | ||||
| @ -1,74 +0,0 @@ | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { MeiliSearch, SearchResponse } from "meilisearch"; | ||||
| import { prettySlug } from "./formatters"; | ||||
| 
 | ||||
| export enum Indexes { | ||||
|   ChronologyEra = "chronology-era", | ||||
|   ChronologyItem = "chronology-item", | ||||
|   Content = "content", | ||||
|   GlossaryItem = "glossary-item", | ||||
|   LibraryItem = "library-item", | ||||
|   MerchItem = "merch-item", | ||||
|   Post = "post", | ||||
|   Video = "video", | ||||
|   VideoChannel = "video-channel", | ||||
|   WeaponStory = "weapon-story", | ||||
| } | ||||
| 
 | ||||
| export function getClient() { | ||||
|   return new MeiliSearch({ | ||||
|     host: process.env.NEXT_PUBLIC_URL_SEARCH ?? "", | ||||
|     apiKey: "", | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export async function getIndexes(client: MeiliSearch) { | ||||
|   return await client.getIndexes(); | ||||
| } | ||||
| 
 | ||||
| export async function search( | ||||
|   client: MeiliSearch, | ||||
|   indexName: Indexes, | ||||
|   query: string | ||||
| ) { | ||||
|   const index = await client.getIndex(indexName); | ||||
|   const results = await index.search(query); | ||||
|   return processSearchResults(results, indexName); | ||||
| } | ||||
| 
 | ||||
| export type SearchResult = { | ||||
|   hits: { | ||||
|     id: string; | ||||
|     href: string; | ||||
|     title: string; | ||||
|     thumbnail?: UploadImageFragment; | ||||
|   }[]; | ||||
|   indexName: string; | ||||
| }; | ||||
| 
 | ||||
| export function processSearchResults( | ||||
|   result: SearchResponse<Record<string, any>>, | ||||
|   indexName: Indexes | ||||
| ): SearchResult { | ||||
|   return { | ||||
|     hits: result.hits.map((hit) => { | ||||
|       switch (indexName) { | ||||
|         case Indexes.Post: { | ||||
|           return { | ||||
|             id: hit.id, | ||||
|             title: | ||||
|               hit.translations.length > 0 | ||||
|                 ? hit.translations[0].title | ||||
|                 : prettySlug(hit.slug), | ||||
|             href: `/news/${hit.slug}`, | ||||
|             thumbnail: hit.thumbnail, | ||||
|           }; | ||||
|         } | ||||
|         default: { | ||||
|           return { id: hit.id, title: prettySlug(hit.slug), href: "error" }; | ||||
|         } | ||||
|       } | ||||
|     }), | ||||
|     indexName: indexName, | ||||
|   }; | ||||
| } | ||||
| @ -3,7 +3,6 @@ import { | ||||
|   GetPostQuery, | ||||
|   GetWikiPageQuery, | ||||
| } from "graphql/generated"; | ||||
| import React from "react"; | ||||
| 
 | ||||
| type Post = NonNullable< | ||||
|   NonNullable<GetPostQuery["posts"]>["data"][number]["attributes"] | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| import { breaks } from "../../design.config"; | ||||
| 
 | ||||
| export function useMediaQuery(query: string): boolean { | ||||
| function useMediaQuery(query: string): boolean { | ||||
|   function getMatches(query: string): boolean { | ||||
|     // Prevents SSR issues
 | ||||
|     if (typeof window !== "undefined") { | ||||
| @ -33,6 +33,7 @@ export function useMediaQuery(query: string): boolean { | ||||
|   return matches; | ||||
| } | ||||
| 
 | ||||
| // ts-unused-exports:disable-next-line
 | ||||
| export function useMediaThin() { | ||||
|   return useMediaQuery(breaks.thin.raw); | ||||
| } | ||||
|  | ||||
							
								
								
									
										84
									
								
								src/hooks/useSmartLanguage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/hooks/useSmartLanguage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { filterDefined, isDefined } from "helpers/others"; | ||||
| 
 | ||||
| import { useRouter } from "next/router"; | ||||
| import { useEffect, useMemo, useState } from "react"; | ||||
| 
 | ||||
| interface Props<T> { | ||||
|   items: T[]; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   languageExtractor: (item: NonNullable<T>) => string | undefined; | ||||
|   transform?: (item: NonNullable<T>) => NonNullable<T>; | ||||
| } | ||||
| 
 | ||||
| function getPreferredLanguage( | ||||
|   preferredLanguages: (string | undefined)[], | ||||
|   availableLanguages: Map<string, number> | ||||
| ): number | undefined { | ||||
|   for (const locale of preferredLanguages) { | ||||
|     if (isDefined(locale) && availableLanguages.has(locale)) { | ||||
|       return availableLanguages.get(locale); | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| export function useSmartLanguage<T>( | ||||
|   props: Props<T> | ||||
| ): [ | ||||
|   T | undefined, | ||||
|   typeof LanguageSwitcher, | ||||
|   Parameters<typeof LanguageSwitcher>[0] | ||||
| ] { | ||||
|   const { | ||||
|     items, | ||||
|     languageExtractor, | ||||
|     languages, | ||||
|     transform = (item) => item, | ||||
|   } = props; | ||||
|   const { preferredLanguages } = useAppLayout(); | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   const availableLocales = useMemo(() => { | ||||
|     const memo = new Map<string, number>(); | ||||
|     filterDefined(items).map((elem, index) => { | ||||
|       const result = languageExtractor(elem); | ||||
|       if (isDefined(result)) memo.set(result, index); | ||||
|     }); | ||||
|     return memo; | ||||
|   }, [items, languageExtractor]); | ||||
| 
 | ||||
|   const [selectedTranslationIndex, setSelectedTranslationIndex] = useState< | ||||
|     number | undefined | ||||
|   >(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setSelectedTranslationIndex( | ||||
|       getPreferredLanguage( | ||||
|         preferredLanguages ?? [router.locale], | ||||
|         availableLocales | ||||
|       ) | ||||
|     ); | ||||
|   }, [preferredLanguages, availableLocales, router.locale]); | ||||
| 
 | ||||
|   const selectedTranslation = useMemo(() => { | ||||
|     if (isDefined(selectedTranslationIndex)) { | ||||
|       const item = items[selectedTranslationIndex]; | ||||
|       if (isDefined(item)) { | ||||
|         return transform(item); | ||||
|       } | ||||
|     } | ||||
|     return undefined; | ||||
|   }, [items, selectedTranslationIndex, transform]); | ||||
| 
 | ||||
|   const languageSwitcherProps = { | ||||
|     languages: languages, | ||||
|     locales: availableLocales, | ||||
|     localesIndex: selectedTranslationIndex, | ||||
|     onLanguageChanged: setSelectedTranslationIndex, | ||||
|   }; | ||||
| 
 | ||||
|   return [selectedTranslation, LanguageSwitcher, languageSwitcherProps]; | ||||
| } | ||||
| @ -27,7 +27,11 @@ function getPreferredLanguage( | ||||
| 
 | ||||
| export function useSmartLanguage<T>( | ||||
|   props: Props<T> | ||||
| ): [T | undefined, () => JSX.Element] { | ||||
| ): [ | ||||
|   T | undefined, | ||||
|   typeof LanguageSwitcher, | ||||
|   Parameters<typeof LanguageSwitcher>[0] | ||||
| ] { | ||||
|   const { | ||||
|     items, | ||||
|     languageExtractor, | ||||
| @ -52,12 +56,10 @@ export function useSmartLanguage<T>( | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setSelectedTranslationIndex( | ||||
|       (current) => | ||||
|         current ?? | ||||
|         getPreferredLanguage( | ||||
|           preferredLanguages ?? [router.locale], | ||||
|           availableLocales | ||||
|         ) | ||||
|       getPreferredLanguage( | ||||
|         preferredLanguages ?? [router.locale], | ||||
|         availableLocales | ||||
|       ) | ||||
|     ); | ||||
|   }, [preferredLanguages, availableLocales, router.locale]); | ||||
| 
 | ||||
| @ -71,15 +73,12 @@ export function useSmartLanguage<T>( | ||||
|     return undefined; | ||||
|   }, [items, selectedTranslationIndex, transform]); | ||||
| 
 | ||||
|   return [ | ||||
|     selectedTranslation, | ||||
|     () => ( | ||||
|       <LanguageSwitcher | ||||
|         languages={languages} | ||||
|         locales={availableLocales} | ||||
|         localesIndex={selectedTranslationIndex} | ||||
|         onLanguageChanged={setSelectedTranslationIndex} | ||||
|       /> | ||||
|     ), | ||||
|   ]; | ||||
|   const languageSwitcherProps = { | ||||
|     languages: languages, | ||||
|     locales: availableLocales, | ||||
|     localesIndex: selectedTranslationIndex, | ||||
|     onLanguageChanged: setSelectedTranslationIndex, | ||||
|   }; | ||||
| 
 | ||||
|   return [selectedTranslation, LanguageSwitcher, languageSwitcherProps]; | ||||
| } | ||||
|  | ||||
| @ -17,7 +17,6 @@ import { ThumbnailHeader } from "components/ThumbnailHeader"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { getNextContent, getPreviousContent } from "helpers/contents"; | ||||
| import { getDescription } from "helpers/description"; | ||||
| import { | ||||
|   prettyinlineTitle, | ||||
| @ -27,6 +26,7 @@ import { | ||||
| } from "helpers/formatters"; | ||||
| import { isUntangibleGroupItem } from "helpers/libraryItem"; | ||||
| import { | ||||
|   filterDefined, | ||||
|   filterHasAttributes, | ||||
|   getStatusDescription, | ||||
|   isDefinedAndNotEmpty, | ||||
| @ -40,21 +40,34 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   content: ContentWithTranslations; | ||||
| } | ||||
| 
 | ||||
| type Group = NonNullable< | ||||
|   NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable<ContentWithTranslations["group"]>["data"] | ||||
|     >["attributes"] | ||||
|   >["contents"] | ||||
| >["data"]; | ||||
| 
 | ||||
| export default function Content(props: Props): JSX.Element { | ||||
|   const { langui, content, languages, currencies } = props; | ||||
|   const isMobile = useMediaMobile(); | ||||
| 
 | ||||
|   const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: content.translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
|   const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: content.translations, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<Props["content"]["translations"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   useScrollTopOnChange(AnchorIds.ContentPanel, [selectedTranslation]); | ||||
| 
 | ||||
| @ -305,7 +318,7 @@ export default function Content(props: Props): JSX.Element { | ||||
|             type={content.type} | ||||
|             categories={content.categories} | ||||
|             langui={langui} | ||||
|             languageSwitcher={<LanguageSwitcher />} | ||||
|             languageSwitcher={<LanguageSwitcher {...languageSwitcherProps} />} | ||||
|           /> | ||||
| 
 | ||||
|           {previousContent?.attributes && ( | ||||
| @ -315,14 +328,14 @@ export default function Content(props: Props): JSX.Element { | ||||
|               </h2> | ||||
|               <TranslatedPreviewLine | ||||
|                 href={`/contents/${previousContent.attributes.slug}`} | ||||
|                 translations={previousContent.attributes.translations?.map( | ||||
|                   (translation) => ({ | ||||
|                     pre_title: translation?.pre_title, | ||||
|                     title: translation?.title, | ||||
|                     subtitle: translation?.subtitle, | ||||
|                     language: translation?.language?.data?.attributes?.code, | ||||
|                   }) | ||||
|                 )} | ||||
|                 translations={filterDefined( | ||||
|                   previousContent.attributes.translations | ||||
|                 ).map((translation) => ({ | ||||
|                   pre_title: translation.pre_title, | ||||
|                   title: translation.title, | ||||
|                   subtitle: translation.subtitle, | ||||
|                   language: translation.language?.data?.attributes?.code, | ||||
|                 }))} | ||||
|                 slug={previousContent.attributes.slug} | ||||
|                 languages={languages} | ||||
|                 thumbnail={ | ||||
| @ -368,14 +381,14 @@ export default function Content(props: Props): JSX.Element { | ||||
|               </h2> | ||||
|               <TranslatedPreviewLine | ||||
|                 href={`/contents/${nextContent.attributes.slug}`} | ||||
|                 translations={nextContent.attributes.translations?.map( | ||||
|                   (translation) => ({ | ||||
|                     pre_title: translation?.pre_title, | ||||
|                     title: translation?.title, | ||||
|                     subtitle: translation?.subtitle, | ||||
|                     language: translation?.language?.data?.attributes?.code, | ||||
|                   }) | ||||
|                 )} | ||||
|                 translations={filterDefined( | ||||
|                   nextContent.attributes.translations | ||||
|                 ).map((translation) => ({ | ||||
|                   pre_title: translation.pre_title, | ||||
|                   title: translation.title, | ||||
|                   subtitle: translation.subtitle, | ||||
|                   language: translation.language?.data?.attributes?.code, | ||||
|                 }))} | ||||
|                 slug={nextContent.attributes.slug} | ||||
|                 languages={languages} | ||||
|                 thumbnail={nextContent.attributes.thumbnail?.data?.attributes} | ||||
| @ -413,11 +426,16 @@ export default function Content(props: Props): JSX.Element { | ||||
|       content.thumbnail?.data?.attributes, | ||||
|       content.type, | ||||
|       isMobile, | ||||
|       languageSwitcherProps, | ||||
|       languages, | ||||
|       langui, | ||||
|       nextContent?.attributes, | ||||
|       previousContent?.attributes, | ||||
|       selectedTranslation, | ||||
|       selectedTranslation?.description, | ||||
|       selectedTranslation?.pre_title, | ||||
|       selectedTranslation?.subtitle, | ||||
|       selectedTranslation?.text_set?.text, | ||||
|       selectedTranslation?.title, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
| @ -487,3 +505,23 @@ export async function getStaticPaths( | ||||
|     fallback: "blocking", | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function getPreviousContent(group: Group, currentSlug: string) { | ||||
|   for (let index = 0; index < group.length; index += 1) { | ||||
|     const content = group[index]; | ||||
|     if (content.attributes?.slug === currentSlug && index > 0) { | ||||
|       return group[index - 1]; | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| function getNextContent(group: Group, currentSlug: string) { | ||||
|   for (let index = 0; index < group.length; index += 1) { | ||||
|     const content = group[index]; | ||||
|     if (content.attributes?.slug === currentSlug && index < group.length - 1) { | ||||
|       return group[index + 1]; | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,7 @@ import { | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| import { GetContentsQuery } from "graphql/generated"; | ||||
| 
 | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyinlineTitle, prettySlug } from "helpers/formatters"; | ||||
| @ -27,6 +27,7 @@ import { | ||||
|   mapRemoveEmptyValues, | ||||
| } from "helpers/others"; | ||||
| import { ContentPlaceholder } from "components/PanelComponents/ContentPlaceholder"; | ||||
| import { GetContentsQuery } from "graphql/generated"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   contents: NonNullable<GetContentsQuery["contents"]>["data"]; | ||||
| @ -194,47 +195,53 @@ export default function Contents(props: Props): JSX.Element { | ||||
|                 > | ||||
|                   {filterHasAttributes(items).map((item) => ( | ||||
|                     <Fragment key={item.id}> | ||||
|                       <TranslatedPreviewCard | ||||
|                         href={`/contents/${item.attributes.slug}`} | ||||
|                         translations={item.attributes.translations?.map( | ||||
|                           (translation) => ({ | ||||
|                             pre_title: translation?.pre_title, | ||||
|                             title: translation?.title, | ||||
|                             subtitle: translation?.subtitle, | ||||
|                             language: | ||||
|                               translation?.language?.data?.attributes?.code, | ||||
|                           }) | ||||
|                         )} | ||||
|                         slug={item.attributes.slug} | ||||
|                         languages={languages} | ||||
|                         thumbnail={item.attributes.thumbnail?.data?.attributes} | ||||
|                         thumbnailAspectRatio="3/2" | ||||
|                         thumbnailForceAspectRatio | ||||
|                         stackNumber={ | ||||
|                           effectiveCombineRelatedContent && | ||||
|                           item.attributes.group?.data?.attributes?.combine === | ||||
|                             true | ||||
|                             ? item.attributes.group.data.attributes.contents | ||||
|                                 ?.data.length | ||||
|                             : 0 | ||||
|                         } | ||||
|                         topChips={ | ||||
|                           item.attributes.type?.data?.attributes | ||||
|                             ? [ | ||||
|                                 item.attributes.type.data.attributes.titles?.[0] | ||||
|                                   ? item.attributes.type.data.attributes | ||||
|                                       .titles[0]?.title | ||||
|                                   : prettySlug( | ||||
|                                       item.attributes.type.data.attributes.slug | ||||
|                                     ), | ||||
|                               ] | ||||
|                             : undefined | ||||
|                         } | ||||
|                         bottomChips={item.attributes.categories?.data.map( | ||||
|                           (category) => category.attributes?.short ?? "" | ||||
|                         )} | ||||
|                         keepInfoVisible={keepInfoVisible} | ||||
|                       /> | ||||
|                       {item.attributes.translations && ( | ||||
|                         <TranslatedPreviewCard | ||||
|                           href={`/contents/${item.attributes.slug}`} | ||||
|                           translations={item.attributes.translations.map( | ||||
|                             (translation) => ({ | ||||
|                               pre_title: translation?.pre_title, | ||||
|                               title: translation?.title, | ||||
|                               subtitle: translation?.subtitle, | ||||
|                               language: | ||||
|                                 translation?.language?.data?.attributes?.code, | ||||
|                             }) | ||||
|                           )} | ||||
|                           slug={item.attributes.slug} | ||||
|                           languages={languages} | ||||
|                           thumbnail={ | ||||
|                             item.attributes.thumbnail?.data?.attributes | ||||
|                           } | ||||
|                           thumbnailAspectRatio="3/2" | ||||
|                           thumbnailForceAspectRatio | ||||
|                           stackNumber={ | ||||
|                             effectiveCombineRelatedContent && | ||||
|                             item.attributes.group?.data?.attributes?.combine === | ||||
|                               true | ||||
|                               ? item.attributes.group.data.attributes.contents | ||||
|                                   ?.data.length | ||||
|                               : 0 | ||||
|                           } | ||||
|                           topChips={ | ||||
|                             item.attributes.type?.data?.attributes | ||||
|                               ? [ | ||||
|                                   item.attributes.type.data.attributes | ||||
|                                     .titles?.[0] | ||||
|                                     ? item.attributes.type.data.attributes | ||||
|                                         .titles[0]?.title | ||||
|                                     : prettySlug( | ||||
|                                         item.attributes.type.data.attributes | ||||
|                                           .slug | ||||
|                                       ), | ||||
|                                 ] | ||||
|                               : undefined | ||||
|                           } | ||||
|                           bottomChips={item.attributes.categories?.data.map( | ||||
|                             (category) => category.attributes?.short ?? "" | ||||
|                           )} | ||||
|                           keepInfoVisible={keepInfoVisible} | ||||
|                         /> | ||||
|                       )} | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
| @ -243,14 +250,7 @@ export default function Contents(props: Props): JSX.Element { | ||||
|         )} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       effectiveCombineRelatedContent, | ||||
|       groups, | ||||
|       keepInfoVisible, | ||||
|       languages, | ||||
|       langui.result, | ||||
|       langui.results, | ||||
|     ] | ||||
|     [effectiveCombineRelatedContent, groups, keepInfoVisible, languages, langui] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
| @ -287,7 +287,7 @@ export async function getStaticProps( | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function getGroups( | ||||
| export function getGroups( | ||||
|   langui: AppStaticProps["langui"], | ||||
|   groupByType: number, | ||||
|   items: Props["contents"] | ||||
| @ -349,7 +349,7 @@ function getGroups( | ||||
|   return mapRemoveEmptyValues(groups); | ||||
| } | ||||
| 
 | ||||
| function filterContents( | ||||
| export function filterContents( | ||||
|   contents: Props["contents"], | ||||
|   combineRelatedContent: boolean, | ||||
|   searchName: string | ||||
|  | ||||
| @ -4,7 +4,6 @@ import { Img } from "components/Img"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { ContentLine } from "components/Library/ContentLine"; | ||||
| import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { | ||||
| @ -31,14 +30,17 @@ import { | ||||
|   prettyItemSubType, | ||||
|   prettyItemType, | ||||
|   prettyPrice, | ||||
|   prettySlug, | ||||
|   prettyURL, | ||||
| } from "helpers/formatters"; | ||||
| import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { convertMmToInch } from "helpers/numbers"; | ||||
| import { | ||||
|   filterDefined, | ||||
|   filterHasAttributes, | ||||
|   isDefined, | ||||
|   isDefinedAndNotEmpty, | ||||
|   isUndefined, | ||||
|   sortContent, | ||||
| } from "helpers/others"; | ||||
| 
 | ||||
| @ -49,10 +51,14 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { Fragment, useMemo, useState } from "react"; | ||||
| import { Fragment, useCallback, useMemo, useState } from "react"; | ||||
| import { isUntangibleGroupItem } from "helpers/libraryItem"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { WithLabel } from "components/Inputs/WithLabel"; | ||||
| import { useToggle } from "hooks/useToggle"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { cJoin, cIf } from "helpers/className"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   item: NonNullable< | ||||
| @ -66,7 +72,7 @@ interface Props extends AppStaticProps { | ||||
| } | ||||
| 
 | ||||
| export default function LibrarySlug(props: Props): JSX.Element { | ||||
|   const { item, itemId, langui, currencies } = props; | ||||
|   const { item, itemId, langui, currencies, languages } = props; | ||||
|   const appLayout = useAppLayout(); | ||||
|   const hoverable = useMediaHoverable(); | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
| @ -501,14 +507,56 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|                 /> | ||||
|               )} | ||||
|               <div className="grid w-full gap-4"> | ||||
|                 {item.contents.data.map((content) => ( | ||||
|                   <ContentLine | ||||
|                     langui={langui} | ||||
|                     content={content} | ||||
|                     parentSlug={item.slug} | ||||
|                     key={content.id} | ||||
|                   /> | ||||
|                 ))} | ||||
|                 {filterHasAttributes(item.contents.data).map( | ||||
|                   (rangedContent) => ( | ||||
|                     <ContentLine | ||||
|                       content={ | ||||
|                         rangedContent.attributes.content?.data?.attributes | ||||
|                           ? { | ||||
|                               translations: filterDefined( | ||||
|                                 rangedContent.attributes.content.data.attributes | ||||
|                                   .translations | ||||
|                               ).map((translation) => ({ | ||||
|                                 pre_title: translation.pre_title, | ||||
|                                 title: translation.title, | ||||
|                                 subtitle: translation.subtitle, | ||||
|                                 language: | ||||
|                                   translation.language?.data?.attributes?.code, | ||||
|                               })), | ||||
|                               categories: filterHasAttributes( | ||||
|                                 rangedContent.attributes.content.data.attributes | ||||
|                                   .categories?.data | ||||
|                               ).map((category) => category.attributes.short), | ||||
|                               type: | ||||
|                                 rangedContent.attributes.content.data.attributes | ||||
|                                   .type?.data?.attributes?.titles?.[0]?.title ?? | ||||
|                                 prettySlug( | ||||
|                                   rangedContent.attributes.content.data | ||||
|                                     .attributes.type?.data?.attributes?.slug | ||||
|                                 ), | ||||
|                               slug: rangedContent.attributes.content.data | ||||
|                                 .attributes.slug, | ||||
|                             } | ||||
|                           : undefined | ||||
|                       } | ||||
|                       langui={langui} | ||||
|                       rangeStart={ | ||||
|                         rangedContent.attributes.range[0]?.__typename === | ||||
|                         "ComponentRangePageRange" | ||||
|                           ? `${rangedContent.attributes.range[0].starting_page}` | ||||
|                           : "" | ||||
|                       } | ||||
|                       slug={rangedContent.attributes.slug} | ||||
|                       parentSlug={item.slug} | ||||
|                       key={rangedContent.id} | ||||
|                       languages={languages} | ||||
|                       hasScanSet={ | ||||
|                         isDefined(rangedContent.attributes.scan_set) && | ||||
|                         rangedContent.attributes.scan_set.length > 0 | ||||
|                       } | ||||
|                     /> | ||||
|                   ) | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
| @ -517,16 +565,31 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|     ), | ||||
|     [ | ||||
|       LightBox, | ||||
|       openLightBox, | ||||
|       appLayout.currency, | ||||
|       currencies, | ||||
|       displayOpenScans, | ||||
|       hoverable, | ||||
|       isVariantSet, | ||||
|       item, | ||||
|       itemId, | ||||
|       keepInfoVisible, | ||||
|       langui, | ||||
|       item.thumbnail?.data?.attributes, | ||||
|       item.subitem_of?.data, | ||||
|       item.title, | ||||
|       item.subtitle, | ||||
|       item.metadata, | ||||
|       item.descriptions, | ||||
|       item.urls, | ||||
|       item.gallery, | ||||
|       item.release_date, | ||||
|       item.price, | ||||
|       item.categories, | ||||
|       item.size, | ||||
|       item.subitems, | ||||
|       item.contents, | ||||
|       item.slug, | ||||
|       itemId, | ||||
|       currencies, | ||||
|       appLayout.currency, | ||||
|       isVariantSet, | ||||
|       hoverable, | ||||
|       keepInfoVisible, | ||||
|       displayOpenScans, | ||||
|       openLightBox, | ||||
|       languages, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
| @ -582,3 +645,112 @@ export async function getStaticPaths( | ||||
|     fallback: "blocking", | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| interface ContentLineProps { | ||||
|   content?: { | ||||
|     translations: { | ||||
|       pre_title: string | null | undefined; | ||||
|       title: string; | ||||
|       subtitle: string | null | undefined; | ||||
|       language: string | undefined; | ||||
|     }[]; | ||||
|     categories?: string[]; | ||||
|     type?: string; | ||||
|     slug: string; | ||||
|   }; | ||||
|   rangeStart: string; | ||||
|   parentSlug: string; | ||||
|   slug: string; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   hasScanSet: boolean; | ||||
| } | ||||
| 
 | ||||
| export function ContentLine(props: ContentLineProps): JSX.Element { | ||||
|   const { | ||||
|     rangeStart, | ||||
|     content, | ||||
|     langui, | ||||
|     languages, | ||||
|     hasScanSet, | ||||
|     slug, | ||||
|     parentSlug, | ||||
|   } = props; | ||||
| 
 | ||||
|   const [opened, setOpened] = useState(false); | ||||
|   const toggleOpened = useToggle(setOpened); | ||||
| 
 | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: content?.translations ?? [], | ||||
|     languages: languages, | ||||
|     languageExtractor: useCallback( | ||||
|       ( | ||||
|         item: NonNullable<ContentLineProps["content"]>["translations"][number] | ||||
|       ) => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className={cJoin( | ||||
|         "grid gap-2 rounded-lg px-4", | ||||
|         cIf(opened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade") | ||||
|       )} | ||||
|     > | ||||
|       <div | ||||
|         className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center | ||||
|         gap-4 thin:grid-cols-[auto_auto_1fr_auto]" | ||||
|       > | ||||
|         <a> | ||||
|           <h3 className="cursor-pointer" onClick={toggleOpened}> | ||||
|             {selectedTranslation | ||||
|               ? prettyinlineTitle( | ||||
|                   selectedTranslation.pre_title, | ||||
|                   selectedTranslation.title, | ||||
|                   selectedTranslation.subtitle | ||||
|                 ) | ||||
|               : content | ||||
|               ? prettySlug(content.slug) | ||||
|               : prettySlug(slug, parentSlug)} | ||||
|           </h3> | ||||
|         </a> | ||||
|         <div className="flex flex-row flex-wrap gap-1"> | ||||
|           {content?.categories?.map((category, index) => ( | ||||
|             <Chip key={index}>{category}</Chip> | ||||
|           ))} | ||||
|         </div> | ||||
|         <p className="h-4 w-full border-b-2 border-dotted border-black opacity-30"></p> | ||||
|         <p>{rangeStart}</p> | ||||
|         {content?.type && ( | ||||
|           <Chip className="justify-self-end thin:hidden">{content.type}</Chip> | ||||
|         )} | ||||
|       </div> | ||||
|       <div | ||||
|         className={`grid-flow-col place-content-start place-items-center gap-2 ${ | ||||
|           opened ? "grid" : "hidden" | ||||
|         }`}
 | ||||
|       > | ||||
|         <Ico icon={Icon.SubdirectoryArrowRight} className="text-dark" /> | ||||
| 
 | ||||
|         {hasScanSet || isDefined(content) ? ( | ||||
|           <> | ||||
|             {hasScanSet && ( | ||||
|               <Button | ||||
|                 href={`/library/${parentSlug}/scans#${slug}`} | ||||
|                 text={langui.view_scans} | ||||
|               /> | ||||
|             )} | ||||
|             {isDefined(content) && ( | ||||
|               <Button href={`/contents/${slug}`} text={langui.open_content} /> | ||||
|             )} | ||||
|           </> | ||||
|         ) : ( | ||||
|           "The content is not available" | ||||
|         )} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| 
 | ||||
|   return <></>; | ||||
| } | ||||
|  | ||||
| @ -194,36 +194,36 @@ export default function Library(props: Props): JSX.Element { | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         <ButtonGroup className="mt-4"> | ||||
|           <ToolTip content={langui.only_display_items_i_want}> | ||||
|             <Button | ||||
|               icon={Icon.Favorite} | ||||
|               onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)} | ||||
|               active={filterUserStatus === LibraryItemUserStatus.Want} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip content={langui.only_display_items_i_have}> | ||||
|             <Button | ||||
|               icon={Icon.BackHand} | ||||
|               onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)} | ||||
|               active={filterUserStatus === LibraryItemUserStatus.Have} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip content={langui.only_display_unmarked_items}> | ||||
|             <Button | ||||
|               icon={Icon.RadioButtonUnchecked} | ||||
|               onClick={() => setFilterUserStatus(LibraryItemUserStatus.None)} | ||||
|               active={filterUserStatus === LibraryItemUserStatus.None} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip content={langui.display_all_items}> | ||||
|             <Button | ||||
|               text={"All"} | ||||
|               onClick={() => setFilterUserStatus(undefined)} | ||||
|               active={isUndefined(filterUserStatus)} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|         </ButtonGroup> | ||||
|         {/* TODO: Add "All" to langui */} | ||||
|         <ButtonGroup | ||||
|           className="mt-4" | ||||
|           buttonsProps={[ | ||||
|             { | ||||
|               tooltip: langui.only_display_items_i_want, | ||||
|               icon: Icon.Favorite, | ||||
|               onClick: () => setFilterUserStatus(LibraryItemUserStatus.Want), | ||||
|               active: filterUserStatus === LibraryItemUserStatus.Want, | ||||
|             }, | ||||
|             { | ||||
|               tooltip: langui.only_display_items_i_have, | ||||
|               icon: Icon.BackHand, | ||||
|               onClick: () => setFilterUserStatus(LibraryItemUserStatus.Have), | ||||
|               active: filterUserStatus === LibraryItemUserStatus.Have, | ||||
|             }, | ||||
|             { | ||||
|               tooltip: langui.only_display_unmarked_items, | ||||
|               icon: Icon.RadioButtonUnchecked, | ||||
|               onClick: () => setFilterUserStatus(LibraryItemUserStatus.None), | ||||
|               active: filterUserStatus === LibraryItemUserStatus.None, | ||||
|             }, | ||||
|             { | ||||
|               tooltip: langui.only_display_unmarked_items, | ||||
|               text: "All", | ||||
|               onClick: () => setFilterUserStatus(undefined), | ||||
|               active: isUndefined(filterUserStatus), | ||||
|             }, | ||||
|           ]} | ||||
|         /> | ||||
| 
 | ||||
|         <Button | ||||
|           className="mt-8" | ||||
|  | ||||
| @ -22,7 +22,7 @@ export default function Merch(props: Props): JSX.Element { | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
|    | ||||
| 
 | ||||
|   return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -26,7 +26,7 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| import { useCallback, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   page: WikiPageWithTranslations; | ||||
| @ -35,11 +35,16 @@ interface Props extends AppStaticProps { | ||||
| export default function WikiPage(props: Props): JSX.Element { | ||||
|   const { page, langui, languages } = props; | ||||
| 
 | ||||
|   const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: page.translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
|   const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: page.translations, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<Props["page"]["translations"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
| @ -69,7 +74,7 @@ export default function WikiPage(props: Props): JSX.Element { | ||||
| 
 | ||||
|         <div className="flex place-content-center gap-4"> | ||||
|           <h1 className="text-center text-3xl">{selectedTranslation?.title}</h1> | ||||
|           <LanguageSwitcher /> | ||||
|           <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <HorizontalLine /> | ||||
| @ -123,6 +128,7 @@ export default function WikiPage(props: Props): JSX.Element { | ||||
|     ), | ||||
|     [ | ||||
|       LanguageSwitcher, | ||||
|       languageSwitcherProps, | ||||
|       languages, | ||||
|       langui, | ||||
|       page.categories?.data, | ||||
|  | ||||
| @ -110,26 +110,28 @@ export default function Wiki(props: Props): JSX.Element { | ||||
|           )} | ||||
|           {filterHasAttributes(filteredPages).map((page) => ( | ||||
|             <Fragment key={page.id}> | ||||
|               <TranslatedPreviewCard | ||||
|                 href={`/wiki/${page.attributes.slug}`} | ||||
|                 translations={page.attributes.translations?.map( | ||||
|                   (translation) => ({ | ||||
|                     title: translation?.title, | ||||
|                     description: translation?.summary, | ||||
|                     language: translation?.language?.data?.attributes?.code, | ||||
|                   }) | ||||
|                 )} | ||||
|                 thumbnail={page.attributes.thumbnail?.data?.attributes} | ||||
|                 thumbnailAspectRatio={"4/3"} | ||||
|                 thumbnailRounded | ||||
|                 thumbnailForceAspectRatio | ||||
|                 languages={languages} | ||||
|                 slug={page.attributes.slug} | ||||
|                 keepInfoVisible={keepInfoVisible} | ||||
|                 bottomChips={page.attributes.categories?.data.map( | ||||
|                   (category) => category.attributes?.short ?? "" | ||||
|                 )} | ||||
|               /> | ||||
|               {page.attributes.translations && ( | ||||
|                 <TranslatedPreviewCard | ||||
|                   href={`/wiki/${page.attributes.slug}`} | ||||
|                   translations={page.attributes.translations.map( | ||||
|                     (translation) => ({ | ||||
|                       title: translation?.title, | ||||
|                       description: translation?.summary, | ||||
|                       language: translation?.language?.data?.attributes?.code, | ||||
|                     }) | ||||
|                   )} | ||||
|                   thumbnail={page.attributes.thumbnail?.data?.attributes} | ||||
|                   thumbnailAspectRatio={"4/3"} | ||||
|                   thumbnailRounded | ||||
|                   thumbnailForceAspectRatio | ||||
|                   languages={languages} | ||||
|                   slug={page.attributes.slug} | ||||
|                   keepInfoVisible={keepInfoVisible} | ||||
|                   bottomChips={page.attributes.categories?.data.map( | ||||
|                     (category) => category.attributes?.short ?? "" | ||||
|                   )} | ||||
|                 /> | ||||
|               )} | ||||
|             </Fragment> | ||||
|           ))} | ||||
|         </div> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint