Continue to improve code with hooks

This commit is contained in:
DrMint 2022-06-25 01:25:45 +02:00
parent d0b91f9db6
commit 2443dee83f
30 changed files with 781 additions and 685 deletions

135
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

View File

@ -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]);
*/

View File

@ -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(

View File

@ -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>
);
}

View File

@ -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 <></>;
}

View File

@ -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>

View File

@ -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>

View File

@ -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),

View File

@ -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,

View File

@ -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 (

View File

@ -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 (

View File

@ -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>

View File

@ -106,8 +106,6 @@ const initialState: RequiredNonNullable<AppLayoutState> = {
const AppContext = React.createContext<AppLayoutState>(initialState);
export default AppContext;
export function useAppLayout(): AppLayoutState {
return useContext(AppContext);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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

View File

@ -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(

View File

@ -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,
};
}

View File

@ -3,7 +3,6 @@ import {
GetPostQuery,
GetWikiPageQuery,
} from "graphql/generated";
import React from "react";
type Post = NonNullable<
NonNullable<GetPostQuery["posts"]>["data"][number]["attributes"]

View File

@ -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);
}

View 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];
}

View File

@ -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];
}

View File

@ -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;
}

View File

@ -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

View File

@ -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 <></>;
}

View File

@ -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"

View File

@ -22,7 +22,7 @@ export default function Merch(props: Props): JSX.Element {
),
[langui]
);
return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />;
}

View File

@ -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,

View File

@ -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>