Put an end to my useMemo craze + fixed ios
This commit is contained in:
parent
c356679813
commit
6a1be38613
|
@ -25,7 +25,7 @@
|
|||
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
|
||||
|
||||
- Language: [TypeScript](https://www.typescriptlang.org/)
|
||||
- Framework: [Next.js 12](https://nextjs.org/) (React 18)
|
||||
- Framework: [Next.js 13](https://nextjs.org/) (React 18)
|
||||
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
|
||||
- Fetch the GraphQL schema from the GraphQL back-end endpoint
|
||||
- Read the operations and fragments stored as graphql files in the `src/graphql` folder
|
||||
|
|
|
@ -6,7 +6,6 @@ const locales = ["en", "es", "fr", "pt-br", "ja"];
|
|||
|
||||
/* @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
swcMinify: true,
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
i18n: {
|
||||
|
|
|
@ -1 +1,91 @@
|
|||
{"currencies":{"data":[{"id":"1","attributes":{"code":"EUR","symbol":"€","rate_to_usd":1.036166,"display_decimals":true}},{"id":"2","attributes":{"code":"CAD","symbol":"$","rate_to_usd":0.79319156,"display_decimals":true}},{"id":"3","attributes":{"code":"USD","symbol":"$","rate_to_usd":1,"display_decimals":true}},{"id":"4","attributes":{"code":"JPY","symbol":"¥","rate_to_usd":0.0083864261,"display_decimals":false}},{"id":"5","attributes":{"code":"BRL","symbol":"R$","rate_to_usd":0.19904328,"display_decimals":true}},{"id":"6","attributes":{"code":"GBP","symbol":"£","rate_to_usd":1.3181323,"display_decimals":true}},{"id":"7","attributes":{"code":"AUD","symbol":"$","rate_to_usd":0.7422,"display_decimals":true}},{"id":"8","attributes":{"code":"INR","symbol":"₹","rate_to_usd":0.013162881,"display_decimals":false}},{"id":"9","attributes":{"code":"NZD","symbol":"$","rate_to_usd":0.69089984,"display_decimals":true}},{"id":"10","attributes":{"code":"CHF","symbol":"CHF","rate_to_usd":1.0728706,"display_decimals":true}}]}}
|
||||
{
|
||||
"currencies": {
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"attributes": {
|
||||
"code": "EUR",
|
||||
"symbol": "€",
|
||||
"rate_to_usd": 1.036166,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"attributes": {
|
||||
"code": "CAD",
|
||||
"symbol": "$",
|
||||
"rate_to_usd": 0.79319156,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"attributes": { "code": "USD", "symbol": "$", "rate_to_usd": 1, "display_decimals": true }
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"attributes": {
|
||||
"code": "JPY",
|
||||
"symbol": "¥",
|
||||
"rate_to_usd": 0.0083864261,
|
||||
"display_decimals": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"attributes": {
|
||||
"code": "BRL",
|
||||
"symbol": "R$",
|
||||
"rate_to_usd": 0.19904328,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"attributes": {
|
||||
"code": "GBP",
|
||||
"symbol": "£",
|
||||
"rate_to_usd": 1.3181323,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"attributes": {
|
||||
"code": "AUD",
|
||||
"symbol": "$",
|
||||
"rate_to_usd": 0.7422,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"attributes": {
|
||||
"code": "INR",
|
||||
"symbol": "₹",
|
||||
"rate_to_usd": 0.013162881,
|
||||
"display_decimals": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"attributes": {
|
||||
"code": "NZD",
|
||||
"symbol": "$",
|
||||
"rate_to_usd": 0.69089984,
|
||||
"display_decimals": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"attributes": {
|
||||
"code": "CHF",
|
||||
"symbol": "CHF",
|
||||
"rate_to_usd": 1.0728706,
|
||||
"display_decimals": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,36 @@
|
|||
{"languages":{"data":[{"id":"1","attributes":{"name":"French","code":"fr","localized_name":"Français"}},{"id":"2","attributes":{"name":"English","code":"en","localized_name":"English"}},{"id":"3","attributes":{"name":"Japanese","code":"ja","localized_name":"日本語"}},{"id":"4","attributes":{"name":"Spanish","code":"es","localized_name":"Español"}},{"id":"6","attributes":{"name":"Portuguese (Brazil)","code":"pt-br","localized_name":"Português (Brasil)"}},{"id":"8","attributes":{"name":"German","code":"de","localized_name":"Deutsch"}},{"id":"9","attributes":{"name":"Italian","code":"it","localized_name":"Italiano"}},{"id":"10","attributes":{"name":"Russian","code":"ru","localized_name":"русский"}},{"id":"11","attributes":{"name":"Korean","code":"ko","localized_name":"한국어"}},{"id":"12","attributes":{"name":"Chinese (Traditional)","code":"zh-cht","localized_name":"中文(繁體)"}}]}}
|
||||
{
|
||||
"languages": {
|
||||
"data": [
|
||||
{ "id": "1", "attributes": { "name": "French", "code": "fr", "localized_name": "Français" } },
|
||||
{ "id": "2", "attributes": { "name": "English", "code": "en", "localized_name": "English" } },
|
||||
{ "id": "3", "attributes": { "name": "Japanese", "code": "ja", "localized_name": "日本語" } },
|
||||
{ "id": "4", "attributes": { "name": "Spanish", "code": "es", "localized_name": "Español" } },
|
||||
{
|
||||
"id": "6",
|
||||
"attributes": {
|
||||
"name": "Portuguese (Brazil)",
|
||||
"code": "pt-br",
|
||||
"localized_name": "Português (Brasil)"
|
||||
}
|
||||
},
|
||||
{ "id": "8", "attributes": { "name": "German", "code": "de", "localized_name": "Deutsch" } },
|
||||
{
|
||||
"id": "9",
|
||||
"attributes": { "name": "Italian", "code": "it", "localized_name": "Italiano" }
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"attributes": { "name": "Russian", "code": "ru", "localized_name": "русский" }
|
||||
},
|
||||
{ "id": "11", "attributes": { "name": "Korean", "code": "ko", "localized_name": "한국어" } },
|
||||
{
|
||||
"id": "12",
|
||||
"attributes": {
|
||||
"name": "Chinese (Traditional)",
|
||||
"code": "zh-cht",
|
||||
"localized_name": "中文(繁體)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,10 +1,8 @@
|
|||
import Head from "next/head";
|
||||
import { useMemo } from "react";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
import { layout } from "../../design.config";
|
||||
import { Ico, Icon } from "./Ico";
|
||||
import { MainPanel } from "./Panels/MainPanel";
|
||||
import { SafariPopup } from "./Panels/SafariPopup";
|
||||
import { isDefined, isUndefined } from "helpers/others";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph";
|
||||
|
@ -77,10 +75,7 @@ export const AppLayout = ({
|
|||
},
|
||||
});
|
||||
|
||||
const turnSubIntoContent = useMemo(
|
||||
() => isDefined(subPanel) && isUndefined(contentPanel),
|
||||
[contentPanel, subPanel]
|
||||
);
|
||||
const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -227,7 +222,6 @@ export const AppLayout = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<SafariPopup />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Markdown from "markdown-to-jsx";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment, useMemo } from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Img } from "components/Img";
|
||||
|
@ -35,11 +35,8 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
const text = useMemo(
|
||||
() => `${preprocessMarkDawn(rawText, playerName)}
|
||||
`,
|
||||
[playerName, rawText]
|
||||
);
|
||||
const text = `${preprocessMarkDawn(rawText, playerName)}
|
||||
`;
|
||||
/* eslint-enable no-irregular-whitespace */
|
||||
|
||||
if (isUndefined(text) || text === "") {
|
||||
|
@ -219,19 +216,17 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
interface TableOfContentsProps {
|
||||
text: string;
|
||||
title?: string;
|
||||
|
||||
horizontalLine?: boolean;
|
||||
}
|
||||
|
||||
export const TableOfContents = ({
|
||||
text,
|
||||
title,
|
||||
|
||||
horizontalLine = false,
|
||||
}: TableOfContentsProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const toc = useMemo(() => getTocFromMarkdawn(preprocessMarkDawn(text), title), [text, title]);
|
||||
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -268,27 +263,24 @@ interface HeaderProps {
|
|||
|
||||
const Header = ({ level, title, slug }: HeaderProps): JSX.Element => {
|
||||
const isHoverable = useDeviceSupportsHover();
|
||||
const innerComponent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div className="ml-10 flex place-items-center gap-4">
|
||||
{title === "* * *" ? (
|
||||
<div className="mt-8 mb-12 space-x-3 text-dark">
|
||||
<Ico icon={Icon.Emergency} />
|
||||
<Ico icon={Icon.Emergency} />
|
||||
<Ico icon={Icon.Emergency} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-headers">{title}</div>
|
||||
)}
|
||||
<AnchorShare
|
||||
className={cIf(isHoverable, "opacity-0 transition-opacity group-hover:opacity-100")}
|
||||
id={slug}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
[isHoverable, slug, title]
|
||||
const innerComponent = (
|
||||
<>
|
||||
<div className="ml-10 flex place-items-center gap-4">
|
||||
{title === "* * *" ? (
|
||||
<div className="mt-8 mb-12 space-x-3 text-dark">
|
||||
<Ico icon={Icon.Emergency} />
|
||||
<Ico icon={Icon.Emergency} />
|
||||
<Ico icon={Icon.Emergency} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-headers">{title}</div>
|
||||
)}
|
||||
<AnchorShare
|
||||
className={cIf(isHoverable, "opacity-0 transition-opacity group-hover:opacity-100")}
|
||||
id={slug}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
switch (level) {
|
||||
|
@ -349,8 +341,7 @@ const TocLevel = ({
|
|||
allowIntersection = true,
|
||||
}: LevelProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
|
||||
const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
|
||||
const ids = tocchildren.map((child) => child.slug);
|
||||
const currentIntersection = useIntersectionList(ids);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { MouseEventHandler, useCallback, useMemo } from "react";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
|
@ -39,10 +39,7 @@ export const NavOption = ({
|
|||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const isActive = useMemo(
|
||||
() => active || router.asPath.startsWith(url),
|
||||
[active, router.asPath, url]
|
||||
);
|
||||
const isActive = active || router.asPath.startsWith(url);
|
||||
|
||||
return (
|
||||
<ToolTip
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { useMemo } from "react";
|
||||
import UAParser from "ua-parser-js";
|
||||
import { useIsClient, useSessionStorage } from "usehooks-ts";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { Popup } from "components/Containers/Popup";
|
||||
import { sendAnalytics } from "helpers/analytics";
|
||||
|
||||
export const SafariPopup = (): JSX.Element => {
|
||||
const [hasDisgardedSafariWarning, setHasDisgardedSafariWarning] = useSessionStorage(
|
||||
"hasDisgardedSafariWarning",
|
||||
false
|
||||
);
|
||||
|
||||
const isClient = useIsClient();
|
||||
const isSafari = useMemo<boolean>(() => {
|
||||
if (isClient) {
|
||||
const parser = new UAParser();
|
||||
return parser.getBrowser().name === "Safari" || parser.getOS().name === "iOS";
|
||||
}
|
||||
return false;
|
||||
}, [isClient]);
|
||||
|
||||
return (
|
||||
<Popup isVisible={isSafari && !hasDisgardedSafariWarning}>
|
||||
<h1 className="text-2xl">Hi, you are using Safari!</h1>
|
||||
<p className="max-w-lg text-center">
|
||||
In most cases this wouldn’t be a problem but our website is—for some obscure
|
||||
reason—performing terribly on Safari (WebKit). Because of that, we have decided to display
|
||||
this message instead of letting you have a slow and painful experience. We are looking into
|
||||
the problem, and are hoping to fix this soon.
|
||||
</p>
|
||||
<p>In the meanwhile, if you are using an iPhone/iPad, please try using another device.</p>
|
||||
<p>If you are on macOS, please use another browser such as Firefox or Chrome.</p>
|
||||
|
||||
<Button
|
||||
text="Let me in regardless"
|
||||
className="mt-8"
|
||||
onClick={() => {
|
||||
setHasDisgardedSafariWarning(true);
|
||||
sendAnalytics("Safari", "Disgard warning");
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon } from "components/Ico";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||
|
@ -34,12 +34,8 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
|
||||
const router = useRouter();
|
||||
|
||||
const currencyOptions = useMemo(
|
||||
() =>
|
||||
filterHasAttributes(currencies, ["attributes"] as const).map(
|
||||
(currentCurrency) => currentCurrency.attributes.code
|
||||
),
|
||||
[currencies]
|
||||
const currencyOptions = filterHasAttributes(currencies, ["attributes"] as const).map(
|
||||
(currentCurrency) => currentCurrency.attributes.code
|
||||
);
|
||||
|
||||
const [currencySelect, setCurrencySelect] = useState<number>(-1);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "./AppLayout";
|
||||
import { Chip } from "./Chip";
|
||||
import { HorizontalLine } from "./HorizontalLine";
|
||||
|
@ -59,140 +59,104 @@ export const PostPage = ({
|
|||
),
|
||||
});
|
||||
|
||||
const { thumbnail, body, title, excerpt } = useMemo(
|
||||
() => ({
|
||||
thumbnail:
|
||||
selectedTranslation?.thumbnail?.data?.attributes ?? post.thumbnail?.data?.attributes,
|
||||
body: selectedTranslation?.body ?? "",
|
||||
title: selectedTranslation?.title ?? prettySlug(post.slug),
|
||||
excerpt: selectedTranslation?.excerpt ?? "",
|
||||
}),
|
||||
[post.slug, post.thumbnail, selectedTranslation]
|
||||
);
|
||||
const thumbnail =
|
||||
selectedTranslation?.thumbnail?.data?.attributes ?? post.thumbnail?.data?.attributes;
|
||||
const body = selectedTranslation?.body ?? "";
|
||||
const title = selectedTranslation?.title ?? prettySlug(post.slug);
|
||||
const excerpt = selectedTranslation?.excerpt ?? "";
|
||||
|
||||
const subPanel = useMemo(
|
||||
() =>
|
||||
returnHref || returnTitle || displayCredits || displayToc ? (
|
||||
<SubPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton href={returnHref} title={returnTitle} displayOnlyOn={"3ColumnsLayout"} />
|
||||
)}
|
||||
|
||||
{displayCredits && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
|
||||
{selectedTranslation && (
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedTranslation.status, langui)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.authors && post.authors.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{"Authors"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(post.authors.data, ["id", "attributes"] as const).map(
|
||||
(author) => (
|
||||
<Fragment key={author.id}>
|
||||
<RecorderChip recorder={author.attributes} />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{displayToc && <TableOfContents text={body} title={title} horizontalLine />}
|
||||
</SubPanel>
|
||||
) : undefined,
|
||||
[
|
||||
body,
|
||||
displayCredits,
|
||||
displayToc,
|
||||
langui,
|
||||
post.authors,
|
||||
returnHref,
|
||||
returnTitle,
|
||||
selectedTranslation,
|
||||
title,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
const subPanel =
|
||||
returnHref || returnTitle || displayCredits || displayToc ? (
|
||||
<SubPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
<ReturnButton href={returnHref} title={returnTitle} displayOnlyOn={"3ColumnsLayout"} />
|
||||
)}
|
||||
|
||||
{displayThumbnailHeader ? (
|
||||
{displayCredits && (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
categories={post.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLanguageSwitcher && (
|
||||
<div className="grid place-content-end place-items-start">
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
<HorizontalLine />
|
||||
|
||||
{selectedTranslation && (
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedTranslation.status, langui)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
{displayTitle && (
|
||||
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">{title}</h1>
|
||||
|
||||
{post.authors && post.authors.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{"Authors"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(post.authors.data, ["id", "attributes"] as const).map(
|
||||
(author) => (
|
||||
<Fragment key={author.id}>
|
||||
<RecorderChip recorder={author.attributes} />
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{prependBody}
|
||||
{body && (
|
||||
<>
|
||||
{displayThumbnailHeader && <HorizontalLine />}
|
||||
<Markdawn text={body} />
|
||||
</>
|
||||
)}
|
||||
{displayToc && <TableOfContents text={body} title={title} horizontalLine />}
|
||||
</SubPanel>
|
||||
) : undefined;
|
||||
|
||||
{appendBody}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
appendBody,
|
||||
body,
|
||||
displayLanguageSwitcher,
|
||||
displayThumbnailHeader,
|
||||
displayTitle,
|
||||
excerpt,
|
||||
languageSwitcherProps,
|
||||
post.categories,
|
||||
prependBody,
|
||||
returnHref,
|
||||
returnTitle,
|
||||
thumbnail,
|
||||
title,
|
||||
]
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayThumbnailHeader ? (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
categories={post.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLanguageSwitcher && (
|
||||
<div className="grid place-content-end place-items-start">
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
</div>
|
||||
)}
|
||||
{displayTitle && (
|
||||
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">{title}</h1>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{prependBody}
|
||||
{body && (
|
||||
<>
|
||||
{displayThumbnailHeader && <HorizontalLine />}
|
||||
<Markdawn text={body} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{appendBody}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout {...otherProps} contentPanel={contentPanel} subPanel={subPanel} />;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Chip } from "./Chip";
|
||||
import { Ico, Icon } from "./Ico";
|
||||
|
@ -75,40 +75,37 @@ export const PreviewCard = ({
|
|||
const isHoverable = useDeviceSupportsHover();
|
||||
const router = useRouter();
|
||||
|
||||
const metadataJSX = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{metadata && (metadata.releaseDate || metadata.price) && (
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
||||
{metadata.releaseDate && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyDate(metadata.releaseDate, router.locale)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.price && (
|
||||
<p className="justify-self-end text-sm">
|
||||
<Ico icon={Icon.ShoppingCart} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyPrice(metadata.price, currencies, currency)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.views && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyShortenNumber(metadata.views)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.author && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Person} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{metadata.author}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[currencies, currency, metadata, router.locale]
|
||||
const metadataJSX = (
|
||||
<>
|
||||
{metadata && (metadata.releaseDate || metadata.price) && (
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
||||
{metadata.releaseDate && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyDate(metadata.releaseDate, router.locale)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.price && (
|
||||
<p className="justify-self-end text-sm">
|
||||
<Ico icon={Icon.ShoppingCart} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyPrice(metadata.price, currencies, currency)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.views && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyShortenNumber(metadata.views)}
|
||||
</p>
|
||||
)}
|
||||
{metadata.author && (
|
||||
<p className="text-sm">
|
||||
<Ico icon={Icon.Person} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{metadata.author}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { Chip } from "./Chip";
|
||||
|
@ -87,17 +87,11 @@ export const SmartList = <T,>({
|
|||
return items;
|
||||
}, [items, searchingBy, searchingCaseInsensitive, searchingTerm]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const filteredBySearch = searchFilter();
|
||||
return filteredBySearch.filter(filteringFunction);
|
||||
}, [filteringFunction, searchFilter]);
|
||||
const filteredItems = searchFilter().filter(filteringFunction);
|
||||
|
||||
const sortedItem = useMemo(
|
||||
() => filteredItems.sort(sortingFunction),
|
||||
[filteredItems, sortingFunction]
|
||||
);
|
||||
const sortedItem = filteredItems.sort(sortingFunction);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const groups = (() => {
|
||||
const memo: Group<T>[] = [];
|
||||
|
||||
sortedItem.forEach((item) => {
|
||||
|
@ -116,9 +110,9 @@ export const SmartList = <T,>({
|
|||
});
|
||||
});
|
||||
return memo.sort(groupSortingFunction);
|
||||
}, [groupCountingFunction, groupSortingFunction, groupingFunction, sortedItem]);
|
||||
})();
|
||||
|
||||
const pages = useMemo(() => {
|
||||
const pages = (() => {
|
||||
const memo: Group<T>[][] = [];
|
||||
let currentPage: Group<T>[] = [];
|
||||
let remainingSlots = paginationItemPerPage;
|
||||
|
@ -162,7 +156,7 @@ export const SmartList = <T,>({
|
|||
}
|
||||
|
||||
return memo;
|
||||
}, [groups, paginationItemPerPage]);
|
||||
})();
|
||||
|
||||
useHotkeys("left", () => setPage((current) => current - 1), { enabled: page > 0 });
|
||||
useHotkeys("right", () => setPage((current) => current + 1), {
|
||||
|
|
|
@ -84,6 +84,7 @@ export const useSettings = (): void => {
|
|||
useEffect(() => {
|
||||
if (preferredLanguages.length === 0) {
|
||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||
console.log(router.locale, getDefaultPreferredLanguages(router.locale, router.locales));
|
||||
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
||||
}
|
||||
} else if (router.locale !== preferredLanguages[0]) {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { useLayoutEffect } from "react";
|
||||
import { isDefined } from "helpers/others";
|
||||
import { useIsWebkit } from "hooks/useIsWebkit";
|
||||
|
||||
export const useWebkitFixes = (): void => {
|
||||
const isWebkit = useIsWebkit();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const next = document.getElementById("__next");
|
||||
if (isDefined(next)) {
|
||||
if (isWebkit) {
|
||||
next.classList.add("webkit-fixes");
|
||||
} else {
|
||||
next.classList.remove("webkit-fixes");
|
||||
}
|
||||
}
|
||||
}, [isWebkit]);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
import { isDefined } from "helpers/others";
|
||||
|
||||
|
@ -13,7 +13,7 @@ export const useFullscreen = (
|
|||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const isClient = useIsClient();
|
||||
|
||||
const elem = useMemo(() => (isClient ? document.querySelector(`#${id}`) : null), [id, isClient]);
|
||||
const elem = isClient ? document.querySelector(`#${id}`) : null;
|
||||
|
||||
const requestFullscreen = useCallback(() => elem?.requestFullscreen(), [elem]);
|
||||
const exitFullscreen = useCallback(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { throttle } from "throttle-debounce";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
import { useOnScroll } from "./useOnScroll";
|
||||
|
@ -10,10 +10,7 @@ export const useIntersectionList = (ids: string[]): number => {
|
|||
|
||||
const isClient = useIsClient();
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (isClient ? document.getElementById(Ids.ContentPanel) : null),
|
||||
[isClient]
|
||||
);
|
||||
const contentPanel = isClient ? document.getElementById(Ids.ContentPanel) : null;
|
||||
|
||||
const refreshCurrentIntersection = useCallback(
|
||||
(scroll: number) => {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { useMemo } from "react";
|
||||
import UAParser from "ua-parser-js";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
|
||||
export const useIsWebkit = (): boolean => {
|
||||
const isClient = useIsClient();
|
||||
return useMemo<boolean>(() => {
|
||||
if (isClient) {
|
||||
const parser = new UAParser();
|
||||
return parser.getBrowser().name === "Safari" || parser.getOS().name === "iOS";
|
||||
}
|
||||
return false;
|
||||
}, [isClient]);
|
||||
};
|
|
@ -1,10 +1,10 @@
|
|||
import { useMemo, useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
import { Ids } from "types/ids";
|
||||
|
||||
export const useOnScroll = (id: Ids, onScroll: (scroll: number) => void): void => {
|
||||
const isClient = useIsClient();
|
||||
const elem = useMemo(() => (isClient ? document.querySelector(`#${id}`) : null), [id, isClient]);
|
||||
const elem = isClient ? document.querySelector(`#${id}`) : null;
|
||||
const listener = useCallback(() => {
|
||||
if (elem?.scrollTop) {
|
||||
onScroll(elem.scrollTop);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
|
||||
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { ImageQuality } from "helpers/img";
|
||||
|
||||
|
@ -94,13 +94,8 @@ export const useReaderSettings = (): {
|
|||
setTeint,
|
||||
]);
|
||||
|
||||
const filterSettings = useMemo(
|
||||
() => ({ bookFold, lighting, paperTexture, teint, dropShadow }),
|
||||
[bookFold, dropShadow, lighting, paperTexture, teint]
|
||||
);
|
||||
|
||||
return {
|
||||
filterSettings,
|
||||
filterSettings: { bookFold, lighting, paperTexture, teint, dropShadow },
|
||||
isSidePagesEnabled,
|
||||
pageQuality,
|
||||
toggleBookFold,
|
||||
|
|
|
@ -3,30 +3,34 @@ import { useEffect, useState } from "react";
|
|||
import { useCounter } from "usehooks-ts";
|
||||
import { isDefined } from "helpers/others";
|
||||
|
||||
const NUM_RETRIES = 10;
|
||||
|
||||
export const useScrollIntoView = (): void => {
|
||||
const router = useRouter();
|
||||
const { count, increment } = useCounter(0);
|
||||
const [hasReachedElem, setHasReachedElem] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!hasReachedElem) {
|
||||
const indexHash = router.asPath.indexOf("#");
|
||||
if (indexHash > 0) {
|
||||
const hash = router.asPath.slice(indexHash + 1);
|
||||
const element = document.getElementById(hash);
|
||||
console.log(element);
|
||||
if (isDefined(element)) {
|
||||
console.log(`[useScrollIntoView] ${hash} found`);
|
||||
element.scrollIntoView();
|
||||
setHasReachedElem(true);
|
||||
} else {
|
||||
console.log(`[useScrollIntoView] ${hash} not found`);
|
||||
setTimeout(() => {
|
||||
increment();
|
||||
}, 100);
|
||||
if (count < NUM_RETRIES)
|
||||
if (!hasReachedElem) {
|
||||
const indexHash = router.asPath.indexOf("#");
|
||||
if (indexHash > 0) {
|
||||
const hash = router.asPath.slice(indexHash + 1);
|
||||
if (hash !== "") {
|
||||
const element = document.getElementById(hash);
|
||||
if (isDefined(element)) {
|
||||
console.log(`[useScrollIntoView] ${hash} found`);
|
||||
element.scrollIntoView();
|
||||
setHasReachedElem(true);
|
||||
} else {
|
||||
console.log(`[useScrollIntoView] ${hash} not found`);
|
||||
setTimeout(() => {
|
||||
increment();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [increment, router.asPath, count, hasReachedElem, setHasReachedElem]);
|
||||
}, [router.asPath, hasReachedElem, setHasReachedElem, increment, count]);
|
||||
|
||||
useEffect(() => setHasReachedElem(false), [router.asPath]);
|
||||
};
|
||||
|
|
|
@ -36,7 +36,7 @@ export const useSmartLanguage = <T>({
|
|||
setSelectedTranslationIndex(getPreferredLanguage(preferredLanguages, availableLocales));
|
||||
}, [preferredLanguages, availableLocales, router.locale]);
|
||||
|
||||
const selectedTranslation = useMemo(() => {
|
||||
const selectedTranslation = (() => {
|
||||
if (isDefined(selectedTranslationIndex)) {
|
||||
const item = items[selectedTranslationIndex];
|
||||
if (isDefined(item)) {
|
||||
|
@ -44,7 +44,7 @@ export const useSmartLanguage = <T>({
|
|||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [items, selectedTranslationIndex, transform]);
|
||||
})();
|
||||
|
||||
const languageSwitcherProps = {
|
||||
languages: languages,
|
||||
|
|
|
@ -22,12 +22,14 @@ import { LightBoxProvider } from "contexts/LightBoxProvider";
|
|||
import { SettingsPopup } from "components/Panels/SettingsPopup";
|
||||
import { useSettings } from "contexts/settings";
|
||||
import { useContainerQueries } from "contexts/containerQueries";
|
||||
import { useWebkitFixes } from "contexts/webkitFixes";
|
||||
|
||||
const AccordsLibraryApp = (props: AppProps): JSX.Element => {
|
||||
useLocalData();
|
||||
useAppLayout();
|
||||
useSettings();
|
||||
useContainerQueries();
|
||||
useWebkitFixes();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { NavOption } from "components/PanelComponents/NavOption";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
|
@ -20,20 +19,18 @@ interface Props extends AppLayoutRequired {}
|
|||
|
||||
const Archives = (props: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Inventory}
|
||||
title={langui.archives}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<NavOption title={"Videos"} url="/archives/videos/" border />
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Inventory}
|
||||
title={langui.archives}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<NavOption title={"Videos"} url="/archives/videos/" border />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
return <AppLayout subPanel={subPanel} {...props} />;
|
||||
};
|
||||
export default Archives;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
|
@ -51,80 +51,74 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<PanelHeader
|
||||
icon={Icon.Movie}
|
||||
title={langui.videos}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
<PanelHeader
|
||||
icon={Icon.Movie}
|
||||
title={langui.videos}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search title..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search title..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
||||
</WithLabel>
|
||||
)}
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
||||
</WithLabel>
|
||||
)}
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(channel?.videos?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${item.attributes.uid}`}
|
||||
title={item.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.published_date,
|
||||
views: item.attributes.views,
|
||||
author: channel?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
groupingFunction={() => [channel?.title ?? ""]}
|
||||
paginationItemPerPage={25}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) => item.attributes.title}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[channel?.title, channel?.videos?.data, isContentPanelAtLeast4xl, keepInfoVisible, searchName]
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(channel?.videos?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${item.attributes.uid}`}
|
||||
title={item.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.published_date,
|
||||
views: item.attributes.views,
|
||||
author: channel?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
groupingFunction={() => [channel?.title ?? ""]}
|
||||
paginationItemPerPage={25}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) => item.attributes.title}
|
||||
/>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { SmartList } from "components/SmartList";
|
||||
|
@ -52,75 +52,69 @@ const Videos = ({ videos, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/"
|
||||
title={"Archives"}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/"
|
||||
title={"Archives"}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<PanelHeader icon={Icon.Movie} title="Videos" description={langui.archives_description} />
|
||||
<PanelHeader icon={Icon.Movie} title="Videos" description={langui.archives_description} />
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search title..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search title..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
||||
</WithLabel>
|
||||
)}
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
||||
</WithLabel>
|
||||
)}
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(videos, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${item.attributes.uid}`}
|
||||
title={item.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.published_date,
|
||||
views: item.attributes.views,
|
||||
author: item.attributes.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
paginationItemPerPage={25}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) => item.attributes.title}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[isContentPanelAtLeast4xl, keepInfoVisible, searchName, videos]
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(videos, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${item.attributes.uid}`}
|
||||
title={item.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.published_date,
|
||||
views: item.attributes.views,
|
||||
author: item.attributes.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
paginationItemPerPage={25}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) => item.attributes.title}
|
||||
/>
|
||||
</ContentPanel>
|
||||
);
|
||||
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
|
@ -34,120 +33,101 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
|
|||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const router = useRouter();
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<NavOption title={langui.video} url="#video" border />
|
||||
<NavOption title={langui.channel} url="#channel" border />
|
||||
<NavOption title={langui.description} url="#description" border />
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
<NavOption title={langui.video} url="#video" border />
|
||||
<NavOption title={langui.channel} url="#channel" border />
|
||||
<NavOption title={langui.description} url="#description" border />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
|
||||
{video.gone ? (
|
||||
<video className="w-full" src={getVideoFile(video.uid)} controls />
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
||||
className="aspect-video w-full"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write;
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
|
||||
{video.gone ? (
|
||||
<video className="w-full" src={getVideoFile(video.uid)} controls />
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
||||
className="aspect-video w-full"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write;
|
||||
encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-2 p-6">
|
||||
<h1 className="text-2xl">{video.title}</h1>
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-6">
|
||||
<p>
|
||||
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyDate(video.published_date, router.locale)}
|
||||
</p>
|
||||
<p>
|
||||
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{isContentPanelAtLeast4xl
|
||||
? video.views.toLocaleString()
|
||||
: prettyShortenNumber(video.views)}
|
||||
</p>
|
||||
{video.channel?.data?.attributes && (
|
||||
<p>
|
||||
<Ico icon={Icon.ThumbUp} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{isContentPanelAtLeast4xl
|
||||
? video.likes.toLocaleString()
|
||||
: prettyShortenNumber(video.likes)}
|
||||
</p>
|
||||
)}
|
||||
<a href={`https://youtu.be/${video.uid}`} target="_blank" rel="noreferrer">
|
||||
<Button className="!py-0 !px-3" text={`${langui.view_on} ${video.source}`} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{video.channel?.data?.attributes && (
|
||||
<InsetBox id="channel" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center">
|
||||
<h2 className="text-2xl">{langui.channel}</h2>
|
||||
<div>
|
||||
<Button
|
||||
href={`/archives/videos/c/${video.channel.data.attributes.uid}`}
|
||||
text={video.channel.data.attributes.title}
|
||||
/>
|
||||
<p>
|
||||
{`${video.channel.data.attributes.subscribers.toLocaleString()}
|
||||
${langui.subscribers?.toLowerCase()}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</InsetBox>
|
||||
allowFullScreen
|
||||
/>
|
||||
)}
|
||||
|
||||
<InsetBox id="description" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.description}</h2>
|
||||
<p className="whitespace-pre-line">{video.description}</p>
|
||||
<div className="mt-2 p-6">
|
||||
<h1 className="text-2xl">{video.title}</h1>
|
||||
<div className="flex w-full flex-row flex-wrap gap-x-6">
|
||||
<p>
|
||||
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{prettyDate(video.published_date, router.locale)}
|
||||
</p>
|
||||
<p>
|
||||
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{isContentPanelAtLeast4xl
|
||||
? video.views.toLocaleString()
|
||||
: prettyShortenNumber(video.views)}
|
||||
</p>
|
||||
{video.channel?.data?.attributes && (
|
||||
<p>
|
||||
<Ico icon={Icon.ThumbUp} className="mr-1 translate-y-[.15em] !text-base" />
|
||||
{isContentPanelAtLeast4xl
|
||||
? video.likes.toLocaleString()
|
||||
: prettyShortenNumber(video.likes)}
|
||||
</p>
|
||||
)}
|
||||
<a href={`https://youtu.be/${video.uid}`} target="_blank" rel="noreferrer">
|
||||
<Button className="!py-0 !px-3" text={`${langui.view_on} ${video.source}`} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{video.channel?.data?.attributes && (
|
||||
<InsetBox id="channel" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center">
|
||||
<h2 className="text-2xl">{langui.channel}</h2>
|
||||
<div>
|
||||
<Button
|
||||
href={`/archives/videos/c/${video.channel.data.attributes.uid}`}
|
||||
text={video.channel.data.attributes.title}
|
||||
/>
|
||||
<p>
|
||||
{`${video.channel.data.attributes.subscribers.toLocaleString()}
|
||||
${langui.subscribers?.toLowerCase()}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</InsetBox>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
isContentPanelAtLeast4xl,
|
||||
langui,
|
||||
router.locale,
|
||||
video.channel?.data?.attributes,
|
||||
video.description,
|
||||
video.gone,
|
||||
video.likes,
|
||||
video.published_date,
|
||||
video.source,
|
||||
video.title,
|
||||
video.uid,
|
||||
video.views,
|
||||
]
|
||||
)}
|
||||
|
||||
<InsetBox id="description" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.description}</h2>
|
||||
<p className="whitespace-pre-line">{video.description}</p>
|
||||
</div>
|
||||
</InsetBox>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
|
||||
|
|
|
@ -67,105 +67,80 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element =
|
|||
),
|
||||
});
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
href="/chronicles"
|
||||
title={langui.chronicles}
|
||||
className="mb-10"
|
||||
/>
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
href="/chronicles"
|
||||
title={langui.chronicles}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{isDefined(selectedTranslation) ? (
|
||||
<>
|
||||
<h1 className="mb-16 text-center text-3xl">{selectedTranslation.title}</h1>
|
||||
{isDefined(selectedTranslation) ? (
|
||||
<>
|
||||
<h1 className="mb-16 text-center text-3xl">{selectedTranslation.title}</h1>
|
||||
|
||||
{languageSwitcherProps.locales.size > 1 && (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
)}
|
||||
{languageSwitcherProps.locales.size > 1 && (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
)}
|
||||
|
||||
{isDefined(selectedTranslation.body) && (
|
||||
<Markdawn text={selectedTranslation.body.body} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{selectedContentTranslation && (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
pre_title={selectedContentTranslation.pre_title}
|
||||
title={selectedContentTranslation.title}
|
||||
subtitle={selectedContentTranslation.subtitle}
|
||||
languageSwitcher={
|
||||
ContentLanguageSwitcherProps.locales.size > 1 ? (
|
||||
<ContentLanguageSwitcher {...ContentLanguageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
categories={primaryContent?.categories}
|
||||
type={primaryContent?.type}
|
||||
description={selectedContentTranslation.description}
|
||||
thumbnail={primaryContent?.thumbnail?.data?.attributes}
|
||||
/>
|
||||
{isDefined(selectedTranslation.body) && <Markdawn text={selectedTranslation.body.body} />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{selectedContentTranslation && (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
pre_title={selectedContentTranslation.pre_title}
|
||||
title={selectedContentTranslation.title}
|
||||
subtitle={selectedContentTranslation.subtitle}
|
||||
languageSwitcher={
|
||||
ContentLanguageSwitcherProps.locales.size > 1 ? (
|
||||
<ContentLanguageSwitcher {...ContentLanguageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
categories={primaryContent?.categories}
|
||||
type={primaryContent?.type}
|
||||
description={selectedContentTranslation.description}
|
||||
thumbnail={primaryContent?.thumbnail?.data?.attributes}
|
||||
/>
|
||||
|
||||
{selectedContentTranslation.text_set?.text && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<Markdawn text={selectedContentTranslation.text_set.text} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
selectedTranslation,
|
||||
languageSwitcherProps,
|
||||
LanguageSwitcher,
|
||||
selectedContentTranslation,
|
||||
ContentLanguageSwitcherProps,
|
||||
ContentLanguageSwitcher,
|
||||
primaryContent?.categories,
|
||||
primaryContent?.type,
|
||||
primaryContent?.thumbnail?.data?.attributes,
|
||||
langui,
|
||||
]
|
||||
{selectedContentTranslation.text_set?.text && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<Markdawn text={selectedContentTranslation.text_set.text} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
displayOnlyOn={"3ColumnsLayout"}
|
||||
href="/chronicles"
|
||||
title={langui.chronicles}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton displayOnlyOn={"3ColumnsLayout"} href="/chronicles" title={langui.chronicles} />
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<div className="grid gap-16">
|
||||
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map(
|
||||
(chapter) => (
|
||||
<TranslatedChroniclesList
|
||||
key={chapter.id}
|
||||
chronicles={chapter.attributes.chronicles.data}
|
||||
translations={filterHasAttributes(chapter.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
title: translation.title,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(chapter.attributes.slug) }}
|
||||
currentSlug={chronicle.slug}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</SubPanel>
|
||||
),
|
||||
[chapters, chronicle.slug, langui]
|
||||
<div className="grid gap-16">
|
||||
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map((chapter) => (
|
||||
<TranslatedChroniclesList
|
||||
key={chapter.id}
|
||||
chronicles={chapter.attributes.chronicles.data}
|
||||
translations={filterHasAttributes(chapter.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
title: translation.title,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(chapter.attributes.slug) }}
|
||||
currentSlug={chronicle.slug}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
import { SubPanel } from "components/Containers/SubPanel";
|
||||
|
@ -26,37 +25,32 @@ interface Props extends AppLayoutRequired {
|
|||
|
||||
const Chronicles = ({ chapters, ...otherProps }: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.WatchLater}
|
||||
title={langui.chronicles}
|
||||
description={langui.chronicles_description}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.WatchLater}
|
||||
title={langui.chronicles}
|
||||
description={langui.chronicles_description}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<div className="grid gap-16">
|
||||
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map(
|
||||
(chapter) => (
|
||||
<TranslatedChroniclesList
|
||||
key={chapter.id}
|
||||
chronicles={chapter.attributes.chronicles.data}
|
||||
translations={filterHasAttributes(chapter.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
title: translation.title,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(chapter.attributes.slug) }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</SubPanel>
|
||||
),
|
||||
[chapters, langui]
|
||||
<div className="grid gap-16">
|
||||
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map((chapter) => (
|
||||
<TranslatedChroniclesList
|
||||
key={chapter.id}
|
||||
chronicles={chapter.attributes.chronicles.data}
|
||||
translations={filterHasAttributes(chapter.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
title: translation.title,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(chapter.attributes.slug) }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
return <AppLayout subPanel={subPanel} {...otherProps} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Chip } from "components/Chip";
|
||||
|
@ -62,344 +62,305 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|||
|
||||
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
|
||||
|
||||
const { previousContent, nextContent } = useMemo(
|
||||
() => ({
|
||||
previousContent:
|
||||
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
|
||||
? getPreviousContent(content.folder.data.attributes.contents.data, content.slug)
|
||||
: undefined,
|
||||
nextContent:
|
||||
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
|
||||
? getNextContent(content.folder.data.attributes.contents.data, content.slug)
|
||||
: undefined,
|
||||
}),
|
||||
[content.folder, content.slug]
|
||||
);
|
||||
const previousContent =
|
||||
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
|
||||
? getPreviousContent(content.folder.data.attributes.contents.data, content.slug)
|
||||
: undefined;
|
||||
const nextContent =
|
||||
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
|
||||
? getNextContent(content.folder.data.attributes.contents.data, content.slug)
|
||||
: undefined;
|
||||
|
||||
const returnButtonProps = useMemo(
|
||||
() => ({
|
||||
href: content.folder?.data?.attributes
|
||||
? `/contents/folder/${content.folder.data.attributes.slug}`
|
||||
: "/contents",
|
||||
const returnButtonProps = {
|
||||
href: content.folder?.data?.attributes
|
||||
? `/contents/folder/${content.folder.data.attributes.slug}`
|
||||
: "/contents",
|
||||
|
||||
translations: filterHasAttributes(content.folder?.data?.attributes?.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
title: title.title,
|
||||
})),
|
||||
fallback: {
|
||||
title: content.folder?.data?.attributes
|
||||
? prettySlug(content.folder.data.attributes.slug)
|
||||
: langui.contents,
|
||||
},
|
||||
langui,
|
||||
}),
|
||||
[content.folder?.data?.attributes, langui]
|
||||
);
|
||||
translations: filterHasAttributes(content.folder?.data?.attributes?.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
title: title.title,
|
||||
})),
|
||||
fallback: {
|
||||
title: content.folder?.data?.attributes
|
||||
? prettySlug(content.folder.data.attributes.slug)
|
||||
: langui.contents,
|
||||
},
|
||||
langui,
|
||||
};
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" />
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" />
|
||||
|
||||
{selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div className="grid gap-5">
|
||||
<h2 className="text-xl">
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code ===
|
||||
selectedTranslation.language?.data?.attributes?.code
|
||||
? langui.transcript_notice
|
||||
: langui.translation_notice}
|
||||
</h2>
|
||||
{selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div className="grid gap-5">
|
||||
<h2 className="text-xl">
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code ===
|
||||
selectedTranslation.language?.data?.attributes?.code
|
||||
? langui.transcript_notice
|
||||
: langui.translation_notice}
|
||||
</h2>
|
||||
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code !==
|
||||
selectedTranslation.language?.data?.attributes?.code && (
|
||||
<div className="grid place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.source_language}:</p>
|
||||
<Chip
|
||||
text={prettyLanguage(
|
||||
selectedTranslation.text_set.source_language.data.attributes.code,
|
||||
languages
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedTranslation.text_set.status, langui)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.text_set.status} />
|
||||
</ToolTip>
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code !==
|
||||
selectedTranslation.language?.data?.attributes?.code && (
|
||||
<div className="grid place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.source_language}:</p>
|
||||
<Chip
|
||||
text={prettyLanguage(
|
||||
selectedTranslation.text_set.source_language.data.attributes.code,
|
||||
languages
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.transcribers &&
|
||||
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.transcribers}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
|
||||
{selectedTranslation.text_set.translators &&
|
||||
selectedTranslation.text_set.translators.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.translators}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedTranslation.text_set.status, langui)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedTranslation.text_set.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{selectedTranslation.text_set.proofreaders &&
|
||||
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.proofreaders}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
||||
{selectedTranslation.text_set.transcribers &&
|
||||
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.notes}:</p>
|
||||
<p className="font-headers font-bold">{langui.transcribers}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
<Markdawn text={selectedTranslation.text_set.notes} />
|
||||
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.translators &&
|
||||
selectedTranslation.text_set.translators.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.translators}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.proofreaders &&
|
||||
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.proofreaders}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip recorder={recorder.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.notes}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
<Markdawn text={selectedTranslation.text_set.notes} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedTranslation?.text_set?.text && (
|
||||
<>
|
||||
<TableOfContents
|
||||
text={selectedTranslation.text_set.text}
|
||||
title={prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)}
|
||||
horizontalLine
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{content.ranged_contents?.data && content.ranged_contents.data.length > 0 && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div>
|
||||
<p className="font-headers text-2xl font-bold">{langui.source}</p>
|
||||
<div className="mt-6 grid place-items-center gap-6">
|
||||
{filterHasAttributes(content.ranged_contents.data, [
|
||||
"attributes.library_item.data.attributes",
|
||||
"attributes.library_item.data.id",
|
||||
] as const).map((rangedContent) => {
|
||||
const libraryItem = rangedContent.attributes.library_item.data;
|
||||
return (
|
||||
<div key={libraryItem.attributes.slug} className={cIf(is1ColumnLayout, "w-3/4")}>
|
||||
<PreviewCard
|
||||
href={`/library/${libraryItem.attributes.slug}`}
|
||||
title={libraryItem.attributes.title}
|
||||
subtitle={libraryItem.attributes.subtitle}
|
||||
thumbnail={libraryItem.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
topChips={
|
||||
libraryItem.attributes.metadata &&
|
||||
libraryItem.attributes.metadata.length > 0 &&
|
||||
libraryItem.attributes.metadata[0]
|
||||
? [prettyItemSubType(libraryItem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [
|
||||
"attributes",
|
||||
] as const).map((category) => category.attributes.short)}
|
||||
metadata={{
|
||||
releaseDate: libraryItem.attributes.release_date,
|
||||
price: libraryItem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(libraryItem.attributes.metadata?.[0]) && (
|
||||
<PreviewCardCTAs id={libraryItem.id} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<TranslatedReturnButton
|
||||
{...returnButtonProps}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<ThumbnailHeader
|
||||
thumbnail={content.thumbnail?.data?.attributes}
|
||||
pre_title={selectedTranslation?.pre_title}
|
||||
title={selectedTranslation?.title}
|
||||
subtitle={selectedTranslation?.subtitle}
|
||||
description={selectedTranslation?.description}
|
||||
type={content.type}
|
||||
categories={content.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{previousContent?.attributes && (
|
||||
<div className="mt-12 mb-8 w-full">
|
||||
<h2 className="mb-4 text-center text-2xl">{langui.previous_content}</h2>
|
||||
<TranslatedPreviewLine
|
||||
href={`/contents/${previousContent.attributes.slug}`}
|
||||
translations={filterHasAttributes(previousContent.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(previousContent.attributes.slug),
|
||||
}}
|
||||
thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
|
||||
topChips={
|
||||
isContentPanelAtLeast2xl && previousContent.attributes.type?.data?.attributes
|
||||
? [
|
||||
previousContent.attributes.type.data.attributes.titles?.[0]
|
||||
? previousContent.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(previousContent.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={
|
||||
isContentPanelAtLeast2xl
|
||||
? previousContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation?.text_set?.text && (
|
||||
<>
|
||||
<TableOfContents
|
||||
text={selectedTranslation.text_set.text}
|
||||
title={prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)}
|
||||
horizontalLine
|
||||
<HorizontalLine />
|
||||
<Markdawn text={selectedTranslation.text_set.text} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{nextContent?.attributes && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<h2 className="mb-4 text-center text-2xl">{langui.followup_content}</h2>
|
||||
<TranslatedPreviewLine
|
||||
href={`/contents/${nextContent.attributes.slug}`}
|
||||
translations={filterHasAttributes(nextContent.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: nextContent.attributes.slug }}
|
||||
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
|
||||
topChips={
|
||||
isContentPanelAtLeast2xl && nextContent.attributes.type?.data?.attributes
|
||||
? [
|
||||
nextContent.attributes.type.data.attributes.titles?.[0]
|
||||
? nextContent.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(nextContent.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={
|
||||
isContentPanelAtLeast2xl
|
||||
? nextContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{content.ranged_contents?.data && content.ranged_contents.data.length > 0 && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div>
|
||||
<p className="font-headers text-2xl font-bold">{langui.source}</p>
|
||||
<div className="mt-6 grid place-items-center gap-6">
|
||||
{filterHasAttributes(content.ranged_contents.data, [
|
||||
"attributes.library_item.data.attributes",
|
||||
"attributes.library_item.data.id",
|
||||
] as const).map((rangedContent) => {
|
||||
const libraryItem = rangedContent.attributes.library_item.data;
|
||||
return (
|
||||
<div
|
||||
key={libraryItem.attributes.slug}
|
||||
className={cIf(is1ColumnLayout, "w-3/4")}>
|
||||
<PreviewCard
|
||||
href={`/library/${libraryItem.attributes.slug}`}
|
||||
title={libraryItem.attributes.title}
|
||||
subtitle={libraryItem.attributes.subtitle}
|
||||
thumbnail={libraryItem.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
topChips={
|
||||
libraryItem.attributes.metadata &&
|
||||
libraryItem.attributes.metadata.length > 0 &&
|
||||
libraryItem.attributes.metadata[0]
|
||||
? [prettyItemSubType(libraryItem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [
|
||||
"attributes",
|
||||
] as const).map((category) => category.attributes.short)}
|
||||
metadata={{
|
||||
releaseDate: libraryItem.attributes.release_date,
|
||||
price: libraryItem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(libraryItem.attributes.metadata?.[0]) && (
|
||||
<PreviewCardCTAs id={libraryItem.id} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
content.ranged_contents?.data,
|
||||
languages,
|
||||
langui,
|
||||
returnButtonProps,
|
||||
selectedTranslation,
|
||||
is1ColumnLayout,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<TranslatedReturnButton
|
||||
{...returnButtonProps}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<ThumbnailHeader
|
||||
thumbnail={content.thumbnail?.data?.attributes}
|
||||
pre_title={selectedTranslation?.pre_title}
|
||||
title={selectedTranslation?.title}
|
||||
subtitle={selectedTranslation?.subtitle}
|
||||
description={selectedTranslation?.description}
|
||||
type={content.type}
|
||||
categories={content.categories}
|
||||
languageSwitcher={
|
||||
languageSwitcherProps.locales.size > 1 ? (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{previousContent?.attributes && (
|
||||
<div className="mt-12 mb-8 w-full">
|
||||
<h2 className="mb-4 text-center text-2xl">{langui.previous_content}</h2>
|
||||
<TranslatedPreviewLine
|
||||
href={`/contents/${previousContent.attributes.slug}`}
|
||||
translations={filterHasAttributes(previousContent.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(previousContent.attributes.slug),
|
||||
}}
|
||||
thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
|
||||
topChips={
|
||||
isContentPanelAtLeast2xl && previousContent.attributes.type?.data?.attributes
|
||||
? [
|
||||
previousContent.attributes.type.data.attributes.titles?.[0]
|
||||
? previousContent.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(previousContent.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={
|
||||
isContentPanelAtLeast2xl
|
||||
? previousContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation?.text_set?.text && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<Markdawn text={selectedTranslation.text_set.text} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{nextContent?.attributes && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<h2 className="mb-4 text-center text-2xl">{langui.followup_content}</h2>
|
||||
<TranslatedPreviewLine
|
||||
href={`/contents/${nextContent.attributes.slug}`}
|
||||
translations={filterHasAttributes(nextContent.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: nextContent.attributes.slug }}
|
||||
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
|
||||
topChips={
|
||||
isContentPanelAtLeast2xl && nextContent.attributes.type?.data?.attributes
|
||||
? [
|
||||
nextContent.attributes.type.data.attributes.titles?.[0]
|
||||
? nextContent.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(nextContent.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={
|
||||
isContentPanelAtLeast2xl
|
||||
? nextContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
content.categories,
|
||||
content.thumbnail?.data?.attributes,
|
||||
content.type,
|
||||
isContentPanelAtLeast2xl,
|
||||
languageSwitcherProps,
|
||||
langui,
|
||||
nextContent?.attributes,
|
||||
previousContent?.attributes,
|
||||
returnButtonProps,
|
||||
selectedTranslation?.description,
|
||||
selectedTranslation?.pre_title,
|
||||
selectedTranslation?.subtitle,
|
||||
selectedTranslation?.text_set?.text,
|
||||
selectedTranslation?.title,
|
||||
]
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
|
@ -116,163 +116,134 @@ const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
|
|||
[searchName]
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<Button href="/contents" text={langui.switch_to_folder_view} icon={Icon.Folder} />
|
||||
<Button href="/contents" text={langui.switch_to_folder_view} icon={Icon.Folder} />
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Contents/All", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Contents/All", "Clear search term");
|
||||
}
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Contents/All", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Contents/All", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "Category", langui.type ?? "Type"]}
|
||||
value={groupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
sendAnalytics(
|
||||
"Contents/All",
|
||||
`Change grouping method (${["none", "category", "type"][value + 1]})`
|
||||
);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "Category", langui.type ?? "Type"]}
|
||||
value={groupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
sendAnalytics(
|
||||
"Contents/All",
|
||||
`Change grouping method (${["none", "category", "type"][value + 1]})`
|
||||
);
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("Contents/All", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("Contents/All", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("Contents/All", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui.always_show_info,
|
||||
langui.category,
|
||||
langui.contents,
|
||||
langui.contents_description,
|
||||
langui.group_by,
|
||||
langui.reset_all_filters,
|
||||
langui.search_title,
|
||||
langui.switch_to_folder_view,
|
||||
langui.type,
|
||||
searchName,
|
||||
setKeepInfoVisible,
|
||||
toggleKeepInfoVisible,
|
||||
]
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("Contents/All", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(contents, ["attributes", "id"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"items-end",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)
|
||||
)}
|
||||
groupingFunction={groupingFunction}
|
||||
filteringFunction={filteringFunction}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) =>
|
||||
`
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(contents, ["attributes", "id"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"items-end",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)
|
||||
)}
|
||||
groupingFunction={groupingFunction}
|
||||
filteringFunction={filteringFunction}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) =>
|
||||
`
|
||||
${item.attributes.slug}
|
||||
${filterDefined(item.attributes.translations)
|
||||
.map((translation) =>
|
||||
prettyInlineTitle(translation.pre_title, translation.title, translation.subtitle)
|
||||
)
|
||||
.join(" ")}`
|
||||
}
|
||||
paginationItemPerPage={50}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
isContentPanelAtLeast4xl,
|
||||
contents,
|
||||
filteringFunction,
|
||||
groupingFunction,
|
||||
keepInfoVisible,
|
||||
searchName,
|
||||
]
|
||||
}
|
||||
paginationItemPerPage={50}
|
||||
/>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||
|
@ -37,150 +36,136 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen
|
|||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<Button href="/contents/all" text={langui.switch_to_grid_view} icon={Icon.Apps} />
|
||||
</SubPanel>
|
||||
),
|
||||
[langui.contents, langui.contents_description, langui.switch_to_grid_view]
|
||||
<Button href="/contents/all" text={langui.switch_to_grid_view} icon={Icon.Apps} />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
|
||||
{folder.parent_folder?.data?.attributes && (
|
||||
<>
|
||||
{folder.parent_folder.data.attributes.slug === "root" ? (
|
||||
<Button href="/contents" icon={Icon.Home} />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
|
||||
translations={filterHasAttributes(folder.parent_folder.data.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.parent_folder.data.attributes.slug),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Ico icon={Icon.ChevronRight} />
|
||||
</>
|
||||
)}
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
|
||||
{folder.parent_folder?.data?.attributes && (
|
||||
<>
|
||||
{folder.parent_folder.data.attributes.slug === "root" ? (
|
||||
<Button href="/contents" icon={Icon.Home} />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
|
||||
translations={filterHasAttributes(folder.parent_folder.data.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.parent_folder.data.attributes.slug),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Ico icon={Icon.ChevronRight} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{folder.slug === "root" ? (
|
||||
<Button href="/contents" icon={Icon.Home} active />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
translations={filterHasAttributes(folder.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.slug),
|
||||
}}
|
||||
active
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{folder.slug === "root" ? (
|
||||
<Button href="/contents" icon={Icon.Home} active />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
translations={filterHasAttributes(folder.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.slug),
|
||||
}}
|
||||
active
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewFolder
|
||||
href={`/contents/folder/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
title: title.title,
|
||||
language: title.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"items-end",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}
|
||||
renderWhenEmpty={() => <></>}
|
||||
groupingFunction={() => [langui.folders ?? "Folders"]}
|
||||
/>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.contents?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewFolder
|
||||
href={`/contents/folder/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
title: title.title,
|
||||
language: title.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"items-end",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
renderWhenEmpty={() => <></>}
|
||||
groupingFunction={() => [langui.contents ?? "Contents"]}
|
||||
/>
|
||||
|
||||
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
|
||||
<NoContentNorFolderMessage />
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
folder.contents?.data,
|
||||
folder.parent_folder?.data?.attributes,
|
||||
folder.slug,
|
||||
folder.subfolders?.data,
|
||||
folder.titles,
|
||||
isContentPanelAtLeast4xl,
|
||||
langui,
|
||||
]
|
||||
renderWhenEmpty={() => <></>}
|
||||
groupingFunction={() => [langui.folders ?? "Folders"]}
|
||||
/>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.contents?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
renderWhenEmpty={() => <></>}
|
||||
groupingFunction={() => [langui.contents ?? "Contents"]}
|
||||
/>
|
||||
|
||||
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
|
||||
<NoContentNorFolderMessage />
|
||||
)}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Chip } from "components/Chip";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
|
@ -23,56 +22,53 @@ interface Props extends AppLayoutRequired {
|
|||
}
|
||||
|
||||
const CheckupContents = ({ contents, ...otherProps }: Props): JSX.Element => {
|
||||
const testReport = useMemo(() => testingContent(contents), [contents]);
|
||||
const testReport = testingContent(contents);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
|
||||
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
|
||||
<p />
|
||||
<p />
|
||||
<p className="font-headers">Ref</p>
|
||||
<p className="font-headers">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
|
||||
<p />
|
||||
<p />
|
||||
<p className="font-headers">Ref</p>
|
||||
<p className="font-headers">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
|
||||
{testReport.lines
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => b.severity - a.severity)
|
||||
.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
|
||||
{testReport.lines
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => b.severity - a.severity)
|
||||
.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
|
||||
justify-items-start gap-2">
|
||||
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
|
||||
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip text={line.type} />
|
||||
<Chip
|
||||
className={
|
||||
line.severity === Severity.VeryHigh
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === Severity.High
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === Severity.Medium
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
text={Severity[line.severity]}
|
||||
/>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[testReport.lines, testReport.title]
|
||||
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
|
||||
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip text={line.type} />
|
||||
<Chip
|
||||
className={
|
||||
line.severity === Severity.VeryHigh
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === Severity.High
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === Severity.Medium
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
text={Severity[line.severity]}
|
||||
/>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} {...otherProps} />;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Chip } from "components/Chip";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
|
@ -27,54 +26,51 @@ interface Props extends AppLayoutRequired {
|
|||
const CheckupLibraryItems = ({ libraryItems, ...otherProps }: Props): JSX.Element => {
|
||||
const testReport = testingLibraryItem(libraryItems);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
|
||||
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
|
||||
<p />
|
||||
<p />
|
||||
<p className="font-headers">Ref</p>
|
||||
<p className="font-headers">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
|
||||
<p />
|
||||
<p />
|
||||
<p className="font-headers">Ref</p>
|
||||
<p className="font-headers">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
|
||||
{testReport.lines
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => b.severity - a.severity)
|
||||
.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid
|
||||
{testReport.lines
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => b.severity - a.severity)
|
||||
.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid
|
||||
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2">
|
||||
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
|
||||
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip text={line.type} />
|
||||
<Chip
|
||||
className={
|
||||
line.severity === Severity.VeryHigh
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === Severity.High
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === Severity.Medium
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
text={Severity[line.severity]}
|
||||
/>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[testReport.lines, testReport.title]
|
||||
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
|
||||
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip text={line.type} />
|
||||
<Chip
|
||||
className={
|
||||
line.severity === Severity.VeryHigh
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === Severity.High
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === Severity.Medium
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
text={Severity[line.severity]}
|
||||
/>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} {...otherProps} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import TurndownService from "turndown";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
|
@ -156,250 +156,243 @@ const Editor = (props: Props): JSX.Element => {
|
|||
[transformationWrapper]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<Popup isVisible={converterOpened} onCloseRequest={() => setConverterOpened(false)}>
|
||||
<div className="text-center">
|
||||
<h2 className="mt-4">Convert HTML to markdown</h2>
|
||||
<p>
|
||||
Copy and paste any HTML content (content from web pages) here.
|
||||
<br />
|
||||
The text will immediatly be converted to valid Markdown.
|
||||
<br />
|
||||
You can then copy the converted text and paste it anywhere you want in the editor
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
title="Ouput textarea"
|
||||
onPaste={(event) => {
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
bulletListMarker: "-",
|
||||
emDelimiter: "_",
|
||||
strongDelimiter: "**",
|
||||
});
|
||||
|
||||
let paste = event.clipboardData.getData("text/html");
|
||||
paste = paste.replace(/<!--.*?-->/u, "");
|
||||
paste = turndownService.turndown(paste);
|
||||
paste = paste.replace(/<!--.*?-->/u, "");
|
||||
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
target.value = paste;
|
||||
target.select();
|
||||
event.preventDefault();
|
||||
}}
|
||||
className="h-[50vh] w-[50vw]"
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<div className="mb-4 flex flex-row gap-2">
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<h3 className="text-lg">Headers</h3>
|
||||
<Button onClick={() => preline("# ")} text={"H1"} />
|
||||
<Button onClick={() => preline("## ")} text={"H2"} />
|
||||
<Button onClick={() => preline("### ")} text={"H3"} />
|
||||
<Button onClick={() => preline("#### ")} text={"H4"} />
|
||||
<Button onClick={() => preline("##### ")} text={"H5"} />
|
||||
<Button onClick={() => preline("###### ")} text={"H6"} />
|
||||
</div>
|
||||
}>
|
||||
<Button icon={Icon.Title} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Bold</h3>}>
|
||||
<Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Italic</h3>}>
|
||||
<Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Toggle Inline Code</h3>
|
||||
<p>
|
||||
Makes the text monospace (like text from a computer terminal). Usually used for
|
||||
stylistic purposes in transcripts.
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Insert footnote</h3>
|
||||
<p>When inserted “x”</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
insert("[^x]");
|
||||
appendDoc("\n\n[^x]: This is a footnote.");
|
||||
}}
|
||||
icon={Icon.Superscript}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcripts</h3>
|
||||
<p>
|
||||
Use this to create dialogues and transcripts. Start by adding a container, then
|
||||
add transcript speech line within.
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcript container</h3>
|
||||
</>
|
||||
}>
|
||||
<Button onClick={() => wrap("Transcript", {}, true)} icon={Icon.AddBox} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcript speech line</h3>
|
||||
<p>
|
||||
Use to add a dialogue/transcript line. Change the <kbd>name</kbd> property
|
||||
to chang the name of the speaker
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => wrap("Line", { name: "speaker" })}
|
||||
icon={Icon.RecordVoiceOver}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</>
|
||||
}>
|
||||
<Button icon={Icon.RecordVoiceOver} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Inset box</h3>}>
|
||||
<Button onClick={() => wrap("InsetBox", {}, true)} icon={Icon.CheckBoxOutlineBlank} />
|
||||
</ToolTip>
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Scene break</h3>}>
|
||||
<Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-lg">Links</h3>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">External Link</h3>
|
||||
<p className="text-xs">Provides a link to another webpage / website</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => insert("[Link name](https://domain.com)")}
|
||||
icon={Icon.Link}
|
||||
text={"External"}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink</h3>
|
||||
<p className="text-xs">
|
||||
Interlinks are used to add links to a header within the same document
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", {})}
|
||||
icon={Icon.Link}
|
||||
text={"Internal"}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink (with target)</h3>{" "}
|
||||
<p className="text-xs">
|
||||
Use this one if you want the intralink text to be different from the target
|
||||
header’s name.
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", { target: "target" })}
|
||||
icon={Icon.Link}
|
||||
text="Internal (w/ target)"
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
}>
|
||||
<Button icon={Icon.Link} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Player’s name placeholder</h3>}>
|
||||
<Button onClick={() => insert("@player")} icon={Icon.Person} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Open HTML Converter</h3>}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setConverterOpened(true);
|
||||
}}
|
||||
icon={Icon.Html}
|
||||
/>
|
||||
</ToolTip>
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<Popup isVisible={converterOpened} onCloseRequest={() => setConverterOpened(false)}>
|
||||
<div className="text-center">
|
||||
<h2 className="mt-4">Convert HTML to markdown</h2>
|
||||
<p>
|
||||
Copy and paste any HTML content (content from web pages) here.
|
||||
<br />
|
||||
The text will immediatly be converted to valid Markdown.
|
||||
<br />
|
||||
You can then copy the converted text and paste it anywhere you want in the editor
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
title="Ouput textarea"
|
||||
onPaste={(event) => {
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
bulletListMarker: "-",
|
||||
emDelimiter: "_",
|
||||
strongDelimiter: "**",
|
||||
});
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h2>Editor</h2>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onInput={(event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement;
|
||||
handleInput(textarea.value);
|
||||
}}
|
||||
className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8
|
||||
font-mono text-black outline-none"
|
||||
value={markdown}
|
||||
title="Input textarea"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Preview</h2>
|
||||
<div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8">
|
||||
<Markdawn className="w-full" text={markdown} />
|
||||
let paste = event.clipboardData.getData("text/html");
|
||||
paste = paste.replace(/<!--.*?-->/u, "");
|
||||
paste = turndownService.turndown(paste);
|
||||
paste = paste.replace(/<!--.*?-->/u, "");
|
||||
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
target.value = paste;
|
||||
target.select();
|
||||
event.preventDefault();
|
||||
}}
|
||||
className="h-[50vh] w-[50vw]"
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<div className="mb-4 flex flex-row gap-2">
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<h3 className="text-lg">Headers</h3>
|
||||
<Button onClick={() => preline("# ")} text={"H1"} />
|
||||
<Button onClick={() => preline("## ")} text={"H2"} />
|
||||
<Button onClick={() => preline("### ")} text={"H3"} />
|
||||
<Button onClick={() => preline("#### ")} text={"H4"} />
|
||||
<Button onClick={() => preline("##### ")} text={"H5"} />
|
||||
<Button onClick={() => preline("###### ")} text={"H6"} />
|
||||
</div>
|
||||
}>
|
||||
<Button icon={Icon.Title} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Bold</h3>}>
|
||||
<Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Italic</h3>}>
|
||||
<Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Toggle Inline Code</h3>
|
||||
<p>
|
||||
Makes the text monospace (like text from a computer terminal). Usually used for
|
||||
stylistic purposes in transcripts.
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Insert footnote</h3>
|
||||
<p>When inserted “x”</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
insert("[^x]");
|
||||
appendDoc("\n\n[^x]: This is a footnote.");
|
||||
}}
|
||||
icon={Icon.Superscript}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcripts</h3>
|
||||
<p>
|
||||
Use this to create dialogues and transcripts. Start by adding a container, then add
|
||||
transcript speech line within.
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcript container</h3>
|
||||
</>
|
||||
}>
|
||||
<Button onClick={() => wrap("Transcript", {}, true)} icon={Icon.AddBox} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcript speech line</h3>
|
||||
<p>
|
||||
Use to add a dialogue/transcript line. Change the <kbd>name</kbd> property
|
||||
to chang the name of the speaker
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => wrap("Line", { name: "speaker" })}
|
||||
icon={Icon.RecordVoiceOver}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</>
|
||||
}>
|
||||
<Button icon={Icon.RecordVoiceOver} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Inset box</h3>}>
|
||||
<Button onClick={() => wrap("InsetBox", {}, true)} icon={Icon.CheckBoxOutlineBlank} />
|
||||
</ToolTip>
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Scene break</h3>}>
|
||||
<Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-lg">Links</h3>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">External Link</h3>
|
||||
<p className="text-xs">Provides a link to another webpage / website</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => insert("[Link name](https://domain.com)")}
|
||||
icon={Icon.Link}
|
||||
text={"External"}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink</h3>
|
||||
<p className="text-xs">
|
||||
Interlinks are used to add links to a header within the same document
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button onClick={() => wrap("IntraLink", {})} icon={Icon.Link} text={"Internal"} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink (with target)</h3>{" "}
|
||||
<p className="text-xs">
|
||||
Use this one if you want the intralink text to be different from the target
|
||||
header’s name.
|
||||
</p>
|
||||
</>
|
||||
}>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", { target: "target" })}
|
||||
icon={Icon.Link}
|
||||
text="Internal (w/ target)"
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
}>
|
||||
<Button icon={Icon.Link} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Player’s name placeholder</h3>}>
|
||||
<Button onClick={() => insert("@player")} icon={Icon.Person} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip placement="bottom" content={<h3 className="text-lg">Open HTML Converter</h3>}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setConverterOpened(true);
|
||||
}}
|
||||
icon={Icon.Html}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h2>Editor</h2>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onInput={(event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement;
|
||||
handleInput(textarea.value);
|
||||
}}
|
||||
className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8
|
||||
font-mono text-black outline-none"
|
||||
value={markdown}
|
||||
title="Input textarea"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Preview</h2>
|
||||
<div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8">
|
||||
<Markdawn className="w-full" text={markdown} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<TableOfContents text={markdown} />
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[appendDoc, converterOpened, handleInput, insert, markdown, preline, toggleWrap, wrap]
|
||||
<div className="mt-8">
|
||||
<TableOfContents text={markdown} />
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} {...props} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||
|
@ -359,183 +359,168 @@ const Transcript = (props: Props): JSX.Element => {
|
|||
[updateDisplayedText]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full} className="overflow-hidden !pr-0 !pt-4">
|
||||
<div className="grid grid-flow-col grid-cols-[1fr_5rem]">
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onChange={updateDisplayedText}
|
||||
onClick={updateLineIndex}
|
||||
onKeyUp={updateLineIndex}
|
||||
title="Input textarea"
|
||||
className="whitespace-pre"
|
||||
/>
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full} className="overflow-hidden !pr-0 !pt-4">
|
||||
<div className="grid grid-flow-col grid-cols-[1fr_5rem]">
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onChange={updateDisplayedText}
|
||||
onClick={updateLineIndex}
|
||||
onKeyUp={updateLineIndex}
|
||||
title="Input textarea"
|
||||
className="whitespace-pre"
|
||||
/>
|
||||
|
||||
<p
|
||||
className="h-[80vh] whitespace-nowrap font-[initial] font-bold
|
||||
<p
|
||||
className="h-[80vh] whitespace-nowrap font-[initial] font-bold
|
||||
[writing-mode:vertical-rl] [transform-origin:top_right]"
|
||||
style={{
|
||||
transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`,
|
||||
}}>
|
||||
{text.split("\n")[lineIndex]}
|
||||
</p>
|
||||
style={{
|
||||
transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`,
|
||||
}}>
|
||||
{text.split("\n")[lineIndex]}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap place-items-center gap-4 pr-24">
|
||||
<div className="grid place-items-center">
|
||||
<p>Text offset: {xOffset}px</p>
|
||||
<input
|
||||
title="Font size multiplier"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={xOffset * 10}
|
||||
onChange={(event) => setXOffset(parseInt(event.target.value, 10) / 10)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap place-items-center gap-4 pr-24">
|
||||
<div className="grid place-items-center">
|
||||
<p>Text offset: {xOffset}px</p>
|
||||
<input
|
||||
title="Font size multiplier"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={xOffset * 10}
|
||||
onChange={(event) => setXOffset(parseInt(event.target.value, 10) / 10)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<p>Font size: {fontSize}x</p>
|
||||
<input
|
||||
title="Font size multiplier"
|
||||
type="range"
|
||||
min="1000"
|
||||
max="3000"
|
||||
value={fontSize * SIZE_MULTIPLIER}
|
||||
onChange={(event) => setFontSize(parseInt(event.target.value, 10) / SIZE_MULTIPLIER)}
|
||||
/>
|
||||
</div>
|
||||
<ToolTip content="Automatically convert Western punctuations to Japanese ones.">
|
||||
<Button text=". ⟹ 。" onClick={convertPunctuation} />
|
||||
</ToolTip>
|
||||
<ToolTip content="Swap a kana for one of its variant (different diacritics).">
|
||||
<Button text="か ⟺ が" onClick={toggleDakuten} />
|
||||
</ToolTip>
|
||||
<ToolTip content="Toggle a kana's small form">
|
||||
<Button text="つ ⟺ っ" onClick={toggleSmallForm} />
|
||||
</ToolTip>
|
||||
<ToolTip content="Convert standard characters to their full width variant.">
|
||||
<Button text="123 ⟹ 123" onClick={convertFullWidth} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "「", onClick: () => insert("「") },
|
||||
{ text: "」", onClick: () => insert("」") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "『", onClick: () => insert("『") },
|
||||
{ text: "』", onClick: () => insert("』") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "【", onClick: () => insert("【") },
|
||||
{ text: "】", onClick: () => insert("】") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〖", onClick: () => insert("〖") },
|
||||
{ text: "〗", onClick: () => insert("〗") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〝", onClick: () => insert("〝") },
|
||||
{ text: "〟", onClick: () => insert("〟") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "(", onClick: () => insert("(") },
|
||||
{ text: ")", onClick: () => insert(")") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "⦅", onClick: () => insert("⦅") },
|
||||
{ text: "⦆", onClick: () => insert("⦆") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〈", onClick: () => insert("〈") },
|
||||
{ text: "〉", onClick: () => insert("〉") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "《", onClick: () => insert("《") },
|
||||
{ text: "》", onClick: () => insert("》") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "{", onClick: () => insert("{") },
|
||||
{ text: "}", onClick: () => insert("}") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "[", onClick: () => insert("[") },
|
||||
{ text: "]", onClick: () => insert("]") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〔", onClick: () => insert("〔") },
|
||||
{ text: "〕", onClick: () => insert("〕") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〘", onClick: () => insert("〘") },
|
||||
{ text: "〙", onClick: () => insert("〙") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<Button text={"Quotations"} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<Button text={"。"} onClick={() => insert("。")} />
|
||||
<Button text={"?"} onClick={() => insert("?")} />
|
||||
<Button text={"!"} onClick={() => insert("!")} />
|
||||
<Button text={"⋯"} onClick={() => insert("⋯")} />
|
||||
<Button text={"※"} onClick={() => insert("※")} />
|
||||
<Button text={"♪"} onClick={() => insert("♪")} />
|
||||
<Button text={"・"} onClick={() => insert("・")} />
|
||||
<Button text={"〇"} onClick={() => insert("〇")} />
|
||||
<Button text={'" "'} onClick={() => insert(" ")} />
|
||||
</div>
|
||||
}>
|
||||
<Button text="Insert" />
|
||||
</ToolTip>
|
||||
<div className="grid place-items-center">
|
||||
<p>Font size: {fontSize}x</p>
|
||||
<input
|
||||
title="Font size multiplier"
|
||||
type="range"
|
||||
min="1000"
|
||||
max="3000"
|
||||
value={fontSize * SIZE_MULTIPLIER}
|
||||
onChange={(event) => setFontSize(parseInt(event.target.value, 10) / SIZE_MULTIPLIER)}
|
||||
/>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
convertFullWidth,
|
||||
convertPunctuation,
|
||||
fontSize,
|
||||
insert,
|
||||
lineIndex,
|
||||
text,
|
||||
toggleDakuten,
|
||||
toggleSmallForm,
|
||||
updateDisplayedText,
|
||||
updateLineIndex,
|
||||
xOffset,
|
||||
]
|
||||
<ToolTip content="Automatically convert Western punctuations to Japanese ones.">
|
||||
<Button text=". ⟹ 。" onClick={convertPunctuation} />
|
||||
</ToolTip>
|
||||
<ToolTip content="Swap a kana for one of its variant (different diacritics).">
|
||||
<Button text="か ⟺ が" onClick={toggleDakuten} />
|
||||
</ToolTip>
|
||||
<ToolTip content="Toggle a kana's small form">
|
||||
<Button text="つ ⟺ っ" onClick={toggleSmallForm} />
|
||||
</ToolTip>
|
||||
<ToolTip content="Convert standard characters to their full width variant.">
|
||||
<Button text="123 ⟹ 123" onClick={convertFullWidth} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "「", onClick: () => insert("「") },
|
||||
{ text: "」", onClick: () => insert("」") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "『", onClick: () => insert("『") },
|
||||
{ text: "』", onClick: () => insert("』") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "【", onClick: () => insert("【") },
|
||||
{ text: "】", onClick: () => insert("】") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〖", onClick: () => insert("〖") },
|
||||
{ text: "〗", onClick: () => insert("〗") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〝", onClick: () => insert("〝") },
|
||||
{ text: "〟", onClick: () => insert("〟") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "(", onClick: () => insert("(") },
|
||||
{ text: ")", onClick: () => insert(")") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "⦅", onClick: () => insert("⦅") },
|
||||
{ text: "⦆", onClick: () => insert("⦆") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〈", onClick: () => insert("〈") },
|
||||
{ text: "〉", onClick: () => insert("〉") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "《", onClick: () => insert("《") },
|
||||
{ text: "》", onClick: () => insert("》") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "{", onClick: () => insert("{") },
|
||||
{ text: "}", onClick: () => insert("}") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "[", onClick: () => insert("[") },
|
||||
{ text: "]", onClick: () => insert("]") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〔", onClick: () => insert("〔") },
|
||||
{ text: "〕", onClick: () => insert("〕") },
|
||||
]}
|
||||
/>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{ text: "〘", onClick: () => insert("〘") },
|
||||
{ text: "〙", onClick: () => insert("〙") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<Button text={"Quotations"} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<Button text={"。"} onClick={() => insert("。")} />
|
||||
<Button text={"?"} onClick={() => insert("?")} />
|
||||
<Button text={"!"} onClick={() => insert("!")} />
|
||||
<Button text={"⋯"} onClick={() => insert("⋯")} />
|
||||
<Button text={"※"} onClick={() => insert("※")} />
|
||||
<Button text={"♪"} onClick={() => insert("♪")} />
|
||||
<Button text={"・"} onClick={() => insert("・")} />
|
||||
<Button text={"〇"} onClick={() => insert("〇")} />
|
||||
<Button text={'" "'} onClick={() => insert(" ")} />
|
||||
</div>
|
||||
}>
|
||||
<Button text="Insert" />
|
||||
</ToolTip>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} {...props} contentPanelScroolbar={false} />;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
|
@ -88,511 +88,467 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
|||
useScrollTopOnChange(Ids.ContentPanel, [item]);
|
||||
const currentIntersection = useIntersectionList(intersectionIds);
|
||||
|
||||
const isVariantSet = useMemo(
|
||||
() =>
|
||||
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set",
|
||||
[item.metadata]
|
||||
const isVariantSet =
|
||||
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
|
||||
|
||||
const displayOpenScans = item.contents?.data.some(
|
||||
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||
);
|
||||
|
||||
const displayOpenScans = useMemo(
|
||||
() =>
|
||||
item.contents?.data.some(
|
||||
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||
),
|
||||
[item.contents?.data]
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton href="/library/" title={langui.library} displayOnlyOn="3ColumnsLayout" />
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<div className="grid gap-4">
|
||||
<NavOption
|
||||
title={langui.summary}
|
||||
url={`#${intersectionIds[0]}`}
|
||||
border
|
||||
active={currentIntersection === 0}
|
||||
/>
|
||||
|
||||
{item.gallery && item.gallery.data.length > 0 && (
|
||||
<NavOption
|
||||
title={langui.gallery}
|
||||
url={`#${intersectionIds[1]}`}
|
||||
border
|
||||
active={currentIntersection === 1}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavOption
|
||||
title={langui.details}
|
||||
url={`#${intersectionIds[2]}`}
|
||||
border
|
||||
active={currentIntersection === 2}
|
||||
/>
|
||||
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<NavOption
|
||||
title={isVariantSet ? langui.variants : langui.subitems}
|
||||
url={`#${intersectionIds[3]}`}
|
||||
border
|
||||
active={currentIntersection === 3}
|
||||
/>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<NavOption
|
||||
title={langui.contents}
|
||||
url={`#${intersectionIds[4]}`}
|
||||
border
|
||||
active={currentIntersection === 4}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton href="/library/" title={langui.library} displayOnlyOn="3ColumnsLayout" />
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<div className="grid gap-4">
|
||||
<NavOption
|
||||
title={langui.summary}
|
||||
url={`#${intersectionIds[0]}`}
|
||||
border
|
||||
active={currentIntersection === 0}
|
||||
/>
|
||||
|
||||
{item.gallery && item.gallery.data.length > 0 && (
|
||||
<NavOption
|
||||
title={langui.gallery}
|
||||
url={`#${intersectionIds[1]}`}
|
||||
border
|
||||
active={currentIntersection === 1}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavOption
|
||||
title={langui.details}
|
||||
url={`#${intersectionIds[2]}`}
|
||||
border
|
||||
active={currentIntersection === 2}
|
||||
/>
|
||||
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<NavOption
|
||||
title={isVariantSet ? langui.variants : langui.subitems}
|
||||
url={`#${intersectionIds[3]}`}
|
||||
border
|
||||
active={currentIntersection === 3}
|
||||
/>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<NavOption
|
||||
title={langui.contents}
|
||||
url={`#${intersectionIds[4]}`}
|
||||
border
|
||||
active={currentIntersection === 4}
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative h-[50vh] w-full cursor-pointer drop-shadow-xl shadow-shade",
|
||||
cIf(isContentPanelAtLeast3xl, "mb-16", "h-[60vh]")
|
||||
)}>
|
||||
{item.thumbnail?.data?.attributes ? (
|
||||
<Img
|
||||
src={item.thumbnail.data.attributes}
|
||||
quality={ImageQuality.Large}
|
||||
className="h-full w-full object-contain"
|
||||
onClick={() => {
|
||||
showLightBox([item.thumbnail?.data?.attributes]);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-[21/29.7] w-full rounded-xl bg-light" />
|
||||
)}
|
||||
</div>
|
||||
</SubPanel>
|
||||
),
|
||||
[currentIntersection, isVariantSet, item.contents, item.gallery, item.subitems, langui]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative h-[50vh] w-full cursor-pointer drop-shadow-xl shadow-shade",
|
||||
cIf(isContentPanelAtLeast3xl, "mb-16", "h-[60vh]")
|
||||
)}>
|
||||
{item.thumbnail?.data?.attributes ? (
|
||||
<Img
|
||||
src={item.thumbnail.data.attributes}
|
||||
quality={ImageQuality.Large}
|
||||
className="h-full w-full object-contain"
|
||||
onClick={() => {
|
||||
showLightBox([item.thumbnail?.data?.attributes]);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-[21/29.7] w-full rounded-xl bg-light" />
|
||||
<InsetBox id={intersectionIds[0]} className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
{item.subitem_of?.data[0]?.attributes && (
|
||||
<div className="grid place-items-center">
|
||||
<p>{langui.subitem_of}</p>
|
||||
<Button
|
||||
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
|
||||
text={prettyInlineTitle(
|
||||
"",
|
||||
item.subitem_of.data[0].attributes.title,
|
||||
item.subitem_of.data[0].attributes.subtitle
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid place-items-center text-center">
|
||||
<h1 className="text-3xl">{item.title}</h1>
|
||||
{isDefinedAndNotEmpty(item.subtitle) && <h2 className="text-2xl">{item.subtitle}</h2>}
|
||||
</div>
|
||||
|
||||
{!isUntangibleGroupItem(item.metadata?.[0]) && isDefinedAndNotEmpty(itemId) && (
|
||||
<PreviewCardCTAs id={itemId} expand />
|
||||
)}
|
||||
|
||||
{item.descriptions?.[0] && (
|
||||
<p className="text-justify">{item.descriptions[0].description}</p>
|
||||
)}
|
||||
{!(
|
||||
item.metadata &&
|
||||
item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
(item.metadata[0].subtype?.data?.attributes?.slug === "variant-set" ||
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "relation-set")
|
||||
) && (
|
||||
<>
|
||||
{item.urls?.length ? (
|
||||
<div className="flex flex-row place-items-center gap-3">
|
||||
<p>{langui.available_at}</p>
|
||||
{filterHasAttributes(item.urls, ["url"] as const).map((url, index) => (
|
||||
<Fragment key={index}>
|
||||
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{langui.item_not_available}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</InsetBox>
|
||||
|
||||
<InsetBox id={intersectionIds[0]} className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
{item.subitem_of?.data[0]?.attributes && (
|
||||
<div className="grid place-items-center">
|
||||
<p>{langui.subitem_of}</p>
|
||||
<Button
|
||||
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
|
||||
text={prettyInlineTitle(
|
||||
"",
|
||||
item.subitem_of.data[0].attributes.title,
|
||||
item.subitem_of.data[0].attributes.subtitle
|
||||
)}
|
||||
/>
|
||||
{item.gallery && item.gallery.data.length > 0 && (
|
||||
<div id={intersectionIds[1]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.gallery}</h2>
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
|
||||
gap-8">
|
||||
{filterHasAttributes(item.gallery.data, ["id", "attributes"] as const).map(
|
||||
(galleryItem, index) => (
|
||||
<Fragment key={galleryItem.id}>
|
||||
<div
|
||||
className="relative aspect-square cursor-pointer
|
||||
transition-transform hover:scale-102"
|
||||
onClick={() => {
|
||||
showLightBox(
|
||||
filterHasAttributes(item.gallery?.data, ["attributes"] as const).map(
|
||||
(image) => image.attributes
|
||||
),
|
||||
index
|
||||
);
|
||||
}}>
|
||||
<Img
|
||||
className="h-full w-full rounded-lg bg-light object-cover shadow-md
|
||||
shadow-shade/30 transition-shadow hover:shadow-lg hover:shadow-shade/50"
|
||||
src={galleryItem.attributes}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<InsetBox id={intersectionIds[2]} className="grid place-items-center">
|
||||
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-10">
|
||||
<h2 className="text-center text-2xl">{langui.details}</h2>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid place-items-center gap-y-8",
|
||||
cIf(isContentPanelAtLeast3xl, "grid-flow-col place-content-between")
|
||||
)}>
|
||||
{item.metadata?.[0] && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{langui.type}</h3>
|
||||
<div className="grid grid-flow-col gap-1">
|
||||
<Chip text={prettyItemType(item.metadata[0], langui)} />
|
||||
{"›"}
|
||||
<Chip text={prettyItemSubType(item.metadata[0])} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid place-items-center text-center">
|
||||
<h1 className="text-3xl">{item.title}</h1>
|
||||
{isDefinedAndNotEmpty(item.subtitle) && (
|
||||
<h2 className="text-2xl">{item.subtitle}</h2>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isUntangibleGroupItem(item.metadata?.[0]) && isDefinedAndNotEmpty(itemId) && (
|
||||
<PreviewCardCTAs id={itemId} expand />
|
||||
{item.release_date && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{langui.release_date}</h3>
|
||||
<p>{prettyDate(item.release_date, router.locale)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.descriptions?.[0] && (
|
||||
<p className="text-justify">{item.descriptions[0].description}</p>
|
||||
)}
|
||||
{!(
|
||||
item.metadata &&
|
||||
item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
(item.metadata[0].subtype?.data?.attributes?.slug === "variant-set" ||
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "relation-set")
|
||||
) && (
|
||||
<>
|
||||
{item.urls?.length ? (
|
||||
<div className="flex flex-row place-items-center gap-3">
|
||||
<p>{langui.available_at}</p>
|
||||
{filterHasAttributes(item.urls, ["url"] as const).map((url, index) => (
|
||||
<Fragment key={index}>
|
||||
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{langui.item_not_available}</p>
|
||||
{item.price && (
|
||||
<div className="grid place-content-start place-items-center text-center">
|
||||
<h3 className="text-xl">{langui.price}</h3>
|
||||
<p>
|
||||
{prettyPrice(
|
||||
item.price,
|
||||
currencies,
|
||||
item.price.currency?.data?.attributes?.code
|
||||
)}
|
||||
</p>
|
||||
{item.price.currency?.data?.attributes?.code !== currency && (
|
||||
<p>
|
||||
{prettyPrice(item.price, currencies, currency)} <br />(
|
||||
{langui.calculated?.toLowerCase()})
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InsetBox>
|
||||
|
||||
{item.gallery && item.gallery.data.length > 0 && (
|
||||
<div id={intersectionIds[1]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.gallery}</h2>
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
|
||||
gap-8">
|
||||
{filterHasAttributes(item.gallery.data, ["id", "attributes"] as const).map(
|
||||
(galleryItem, index) => (
|
||||
<Fragment key={galleryItem.id}>
|
||||
<div
|
||||
className="relative aspect-square cursor-pointer
|
||||
transition-transform hover:scale-102"
|
||||
onClick={() => {
|
||||
showLightBox(
|
||||
filterHasAttributes(item.gallery?.data, ["attributes"] as const).map(
|
||||
(image) => image.attributes
|
||||
),
|
||||
index
|
||||
);
|
||||
}}>
|
||||
<Img
|
||||
className="h-full w-full rounded-lg bg-light object-cover shadow-md
|
||||
shadow-shade/30 transition-shadow hover:shadow-lg hover:shadow-shade/50"
|
||||
src={galleryItem.attributes}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
{item.categories && item.categories.data.length > 0 && (
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-xl">{langui.categories}</h3>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(item.categories.data, ["attributes"] as const).map(
|
||||
(category) => (
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
<InsetBox id={intersectionIds[2]} className="grid place-items-center">
|
||||
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-10">
|
||||
<h2 className="text-center text-2xl">{langui.details}</h2>
|
||||
{item.size && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid place-items-center gap-y-8",
|
||||
cIf(isContentPanelAtLeast3xl, "grid-flow-col place-content-between")
|
||||
"grid gap-4",
|
||||
cIf(!isContentPanelAtLeast3xl, "place-items-center")
|
||||
)}>
|
||||
{item.metadata?.[0] && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{langui.type}</h3>
|
||||
<div className="grid grid-flow-col gap-1">
|
||||
<Chip text={prettyItemType(item.metadata[0], langui)} />
|
||||
{"›"}
|
||||
<Chip text={prettyItemSubType(item.metadata[0])} />
|
||||
<h3 className="text-xl">{langui.size}</h3>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid w-full",
|
||||
cIf(
|
||||
isContentPanelAtLeastSm,
|
||||
"grid-flow-col place-content-between",
|
||||
"grid-flow-row place-content-center gap-8"
|
||||
)
|
||||
)}>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-x-4",
|
||||
cIf(
|
||||
isContentPanelAtLeast3xl,
|
||||
"grid-flow-col place-items-start",
|
||||
"place-items-center"
|
||||
)
|
||||
)}>
|
||||
<p className="font-bold">{langui.width}:</p>
|
||||
<div>
|
||||
<p>{item.size.width} mm</p>
|
||||
<p>{convertMmToInch(item.size.width)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.release_date && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{langui.release_date}</h3>
|
||||
<p>{prettyDate(item.release_date, router.locale)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.price && (
|
||||
<div className="grid place-content-start place-items-center text-center">
|
||||
<h3 className="text-xl">{langui.price}</h3>
|
||||
<p>
|
||||
{prettyPrice(
|
||||
item.price,
|
||||
currencies,
|
||||
item.price.currency?.data?.attributes?.code
|
||||
)}
|
||||
</p>
|
||||
{item.price.currency?.data?.attributes?.code !== currency && (
|
||||
<p>
|
||||
{prettyPrice(item.price, currencies, currency)} <br />(
|
||||
{langui.calculated?.toLowerCase()})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item.categories && item.categories.data.length > 0 && (
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-xl">{langui.categories}</h3>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(item.categories.data, ["attributes"] as const).map(
|
||||
(category) => (
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-x-4",
|
||||
cIf(
|
||||
isContentPanelAtLeast3xl,
|
||||
"grid-flow-col place-items-start",
|
||||
"place-items-center"
|
||||
)
|
||||
)}
|
||||
)}>
|
||||
<p className="font-bold">{langui.height}:</p>
|
||||
<div>
|
||||
<p>{item.size.height} mm</p>
|
||||
<p>{convertMmToInch(item.size.height)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
{isDefined(item.size.thickness) && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-x-4",
|
||||
cIf(
|
||||
isContentPanelAtLeast3xl,
|
||||
"grid-flow-col place-items-start",
|
||||
"place-items-center"
|
||||
)
|
||||
)}>
|
||||
<p className="font-bold">{langui.thickness}:</p>
|
||||
<div>
|
||||
<p>{item.size.thickness} mm</p>
|
||||
<p>{convertMmToInch(item.size.thickness)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.size && (
|
||||
{item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
|
||||
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-4",
|
||||
cIf(!isContentPanelAtLeast3xl, "place-items-center")
|
||||
)}>
|
||||
<h3 className="text-xl">{langui.size}</h3>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid w-full",
|
||||
cIf(
|
||||
isContentPanelAtLeastSm,
|
||||
"grid-flow-col place-content-between",
|
||||
"grid-flow-row place-content-center gap-8"
|
||||
)
|
||||
)}>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-x-4",
|
||||
cIf(
|
||||
isContentPanelAtLeast3xl,
|
||||
"grid-flow-col place-items-start",
|
||||
"place-items-center"
|
||||
)
|
||||
)}>
|
||||
<p className="font-bold">{langui.width}:</p>
|
||||
<div>
|
||||
<p>{item.size.width} mm</p>
|
||||
<p>{convertMmToInch(item.size.width)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-x-4",
|
||||
cIf(
|
||||
isContentPanelAtLeast3xl,
|
||||
"grid-flow-col place-items-start",
|
||||
"place-items-center"
|
||||
)
|
||||
)}>
|
||||
<p className="font-bold">{langui.height}:</p>
|
||||
<div>
|
||||
<p>{item.size.height} mm</p>
|
||||
<p>{convertMmToInch(item.size.height)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
{isDefined(item.size.thickness) && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-x-4",
|
||||
cIf(
|
||||
isContentPanelAtLeast3xl,
|
||||
"grid-flow-col place-items-start",
|
||||
"place-items-center"
|
||||
)
|
||||
)}>
|
||||
<p className="font-bold">{langui.thickness}:</p>
|
||||
<div>
|
||||
<p>{item.size.thickness} mm</p>
|
||||
<p>{convertMmToInch(item.size.thickness)} in</p>
|
||||
<h3 className="text-xl">{langui.type_information}</h3>
|
||||
<div className="flex flex-wrap place-content-between gap-x-8">
|
||||
{item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
|
||||
<>
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.pages}:</p>
|
||||
<p>{item.metadata[0].page_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.binding}:</p>
|
||||
<p>
|
||||
{item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Paperback
|
||||
? langui.paperback
|
||||
: item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Hardcover
|
||||
? langui.hardcover
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.page_order}:</p>
|
||||
<p>
|
||||
{item.metadata[0].page_order ===
|
||||
Enum_Componentmetadatabooks_Page_Order.LeftToRight
|
||||
? langui.left_to_right
|
||||
: langui.right_to_left}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.languages}:</p>
|
||||
{item.metadata[0]?.languages?.data.map((lang) => (
|
||||
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InsetBox>
|
||||
|
||||
{item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
|
||||
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-4",
|
||||
cIf(!isContentPanelAtLeast3xl, "place-items-center")
|
||||
)}>
|
||||
<h3 className="text-xl">{langui.type_information}</h3>
|
||||
<div className="flex flex-wrap place-content-between gap-x-8">
|
||||
{item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
|
||||
<>
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.pages}:</p>
|
||||
<p>{item.metadata[0].page_count}</p>
|
||||
</div>
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<div id={intersectionIds[3]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="text-2xl">{isVariantSet ? langui.variants : langui.subitems}</h2>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.binding}:</p>
|
||||
<p>
|
||||
{item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Paperback
|
||||
? langui.paperback
|
||||
: item.metadata[0].binding_type ===
|
||||
Enum_Componentmetadatabooks_Binding_Type.Hardcover
|
||||
? langui.hardcover
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.page_order}:</p>
|
||||
<p>
|
||||
{item.metadata[0].page_order ===
|
||||
Enum_Componentmetadatabooks_Page_Order.LeftToRight
|
||||
? langui.left_to_right
|
||||
: langui.right_to_left}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-start gap-4">
|
||||
<p className="font-bold">{langui.languages}:</p>
|
||||
{item.metadata[0]?.languages?.data.map((lang) => (
|
||||
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InsetBox>
|
||||
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<div id={intersectionIds[3]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="text-2xl">{isVariantSet ? langui.variants : langui.subitems}</h2>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]
|
||||
items-end gap-8">
|
||||
{filterHasAttributes(item.subitems.data, ["id", "attributes"] as const).map(
|
||||
(subitem) => (
|
||||
<Fragment key={subitem.id}>
|
||||
<PreviewCard
|
||||
href={`/library/${subitem.attributes.slug}`}
|
||||
title={subitem.attributes.title}
|
||||
subtitle={subitem.attributes.subtitle}
|
||||
thumbnail={subitem.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
subitem.attributes.metadata &&
|
||||
subitem.attributes.metadata.length > 0 &&
|
||||
subitem.attributes.metadata[0]
|
||||
? [prettyItemSubType(subitem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={subitem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: subitem.attributes.release_date,
|
||||
price: subitem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(subitem.attributes.metadata?.[0]) && (
|
||||
<PreviewCardCTAs id={subitem.id} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
|
||||
{displayOpenScans && (
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Button href={`/library/${item.slug}/reader`} text={langui.view_scans} />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w- grid w-full gap-4">
|
||||
{filterHasAttributes(item.contents.data, ["attributes"] as const).map(
|
||||
(rangedContent) => (
|
||||
<ContentLine
|
||||
content={
|
||||
rangedContent.attributes.content?.data?.attributes
|
||||
? {
|
||||
translations: filterDefined(
|
||||
rangedContent.attributes.content.data.attributes.translations
|
||||
).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language?.data?.attributes?.code,
|
||||
})),
|
||||
categories: filterHasAttributes(
|
||||
rangedContent.attributes.content.data.attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => category.attributes.short),
|
||||
type:
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.titles?.[0]?.title ??
|
||||
prettySlug(
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.slug
|
||||
),
|
||||
slug: rangedContent.attributes.content.data.attributes.slug,
|
||||
}
|
||||
: undefined
|
||||
{filterHasAttributes(item.subitems.data, ["id", "attributes"] as const).map(
|
||||
(subitem) => (
|
||||
<Fragment key={subitem.id}>
|
||||
<PreviewCard
|
||||
href={`/library/${subitem.attributes.slug}`}
|
||||
title={subitem.attributes.title}
|
||||
subtitle={subitem.attributes.subtitle}
|
||||
thumbnail={subitem.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
subitem.attributes.metadata &&
|
||||
subitem.attributes.metadata.length > 0 &&
|
||||
subitem.attributes.metadata[0]
|
||||
? [prettyItemSubType(subitem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
rangeStart={
|
||||
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${rangedContent.attributes.range[0].starting_page}`
|
||||
: ""
|
||||
bottomChips={subitem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: subitem.attributes.release_date,
|
||||
price: subitem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(subitem.attributes.metadata?.[0]) && (
|
||||
<PreviewCardCTAs id={subitem.id} />
|
||||
)
|
||||
}
|
||||
slug={rangedContent.attributes.slug}
|
||||
parentSlug={item.slug}
|
||||
key={rangedContent.id}
|
||||
hasScanSet={
|
||||
isDefined(rangedContent.attributes.scan_set) &&
|
||||
rangedContent.attributes.scan_set.length > 0
|
||||
}
|
||||
condensed={!isContentPanelAtLeast3xl}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
langui,
|
||||
isContentPanelAtLeast3xl,
|
||||
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,
|
||||
router.locale,
|
||||
currencies,
|
||||
currency,
|
||||
isContentPanelAtLeastSm,
|
||||
isVariantSet,
|
||||
hoverable,
|
||||
toggleKeepInfoVisible,
|
||||
keepInfoVisible,
|
||||
displayOpenScans,
|
||||
showLightBox,
|
||||
]
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
|
||||
{displayOpenScans && (
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Button href={`/library/${item.slug}/reader`} text={langui.view_scans} />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w- grid w-full gap-4">
|
||||
{filterHasAttributes(item.contents.data, ["attributes"] as const).map(
|
||||
(rangedContent) => (
|
||||
<ContentLine
|
||||
content={
|
||||
rangedContent.attributes.content?.data?.attributes
|
||||
? {
|
||||
translations: filterDefined(
|
||||
rangedContent.attributes.content.data.attributes.translations
|
||||
).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language?.data?.attributes?.code,
|
||||
})),
|
||||
categories: filterHasAttributes(
|
||||
rangedContent.attributes.content.data.attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => category.attributes.short),
|
||||
type:
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.titles?.[0]?.title ??
|
||||
prettySlug(
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.slug
|
||||
),
|
||||
slug: rangedContent.attributes.content.data.attributes.slug,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rangeStart={
|
||||
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${rangedContent.attributes.range[0].starting_page}`
|
||||
: ""
|
||||
}
|
||||
slug={rangedContent.attributes.slug}
|
||||
parentSlug={item.slug}
|
||||
key={rangedContent.id}
|
||||
hasScanSet={
|
||||
isDefined(rangedContent.attributes.scan_set) &&
|
||||
rangedContent.attributes.scan_set.length > 0
|
||||
}
|
||||
condensed={!isContentPanelAtLeast3xl}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import Slider from "rc-slider";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -44,6 +44,7 @@ import { useFullscreen } from "hooks/useFullscreen";
|
|||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { FilterSettings, useReaderSettings } from "hooks/useReaderSettings";
|
||||
import { useIsWebkit } from "hooks/useIsWebkit";
|
||||
|
||||
const CUSTOM_DARK_DROPSHADOW = `
|
||||
drop-shadow(0 0 0.5em rgb(var(--theme-color-shade) / 30%))
|
||||
|
@ -112,19 +113,14 @@ const LibrarySlug = ({
|
|||
is1ColumnLayout ? "single" : "double"
|
||||
);
|
||||
const router = useRouter();
|
||||
const isWebkit = useIsWebkit();
|
||||
|
||||
const { isFullscreen, toggleFullscreen, requestFullscreen } = useFullscreen(Ids.ContentPanel);
|
||||
|
||||
const effectiveDisplayMode = useMemo(
|
||||
() =>
|
||||
currentPageIndex === 0 || currentPageIndex === pages.length - 1 ? "single" : displayMode,
|
||||
[currentPageIndex, displayMode, pages.length]
|
||||
);
|
||||
const effectiveDisplayMode =
|
||||
currentPageIndex === 0 || currentPageIndex === pages.length - 1 ? "single" : displayMode;
|
||||
|
||||
const ajustedSidepagesTotalWidth = useMemo(
|
||||
() => pages.length * SIDEPAGES_PAGE_WIDTH * (120 / pageWidth),
|
||||
[pageWidth, pages.length]
|
||||
);
|
||||
const ajustedSidepagesTotalWidth = pages.length * SIDEPAGES_PAGE_WIDTH * (120 / pageWidth);
|
||||
|
||||
const changeCurrentPageIndex = useCallback(
|
||||
(callbackFn: (current: number) => number) => {
|
||||
|
@ -185,61 +181,39 @@ const LibrarySlug = ({
|
|||
handlePageNavigation,
|
||||
]);
|
||||
|
||||
const firstPage = useMemo(
|
||||
() =>
|
||||
pages[
|
||||
effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
|
||||
? currentPageIndex - 1
|
||||
: currentPageIndex
|
||||
],
|
||||
[currentPageIndex, effectiveDisplayMode, pages]
|
||||
);
|
||||
const secondPage = useMemo(
|
||||
() =>
|
||||
pages[
|
||||
effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
|
||||
? currentPageIndex
|
||||
: currentPageIndex + 1
|
||||
],
|
||||
[currentPageIndex, effectiveDisplayMode, pages]
|
||||
);
|
||||
const firstPage =
|
||||
pages[
|
||||
effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
|
||||
? currentPageIndex - 1
|
||||
: currentPageIndex
|
||||
];
|
||||
|
||||
const leftSidePagesCount = useMemo(
|
||||
() =>
|
||||
pageOrder === PageOrder.LeftToRight ? currentPageIndex : pages.length - 1 - currentPageIndex,
|
||||
[currentPageIndex, pageOrder, pages.length]
|
||||
);
|
||||
const secondPage =
|
||||
pages[
|
||||
effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
|
||||
? currentPageIndex
|
||||
: currentPageIndex + 1
|
||||
];
|
||||
|
||||
const rightSidePagesCount = useMemo(
|
||||
() =>
|
||||
pageOrder === PageOrder.LeftToRight ? pages.length - 1 - currentPageIndex : currentPageIndex,
|
||||
[currentPageIndex, pageOrder, pages.length]
|
||||
);
|
||||
const leftSidePagesCount =
|
||||
pageOrder === PageOrder.LeftToRight ? currentPageIndex : pages.length - 1 - currentPageIndex;
|
||||
|
||||
const leftSidePagesWidth = useMemo(
|
||||
() =>
|
||||
`${
|
||||
pageOrder === PageOrder.LeftToRight
|
||||
? (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
: ajustedSidepagesTotalWidth -
|
||||
(currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
}vmin`,
|
||||
[ajustedSidepagesTotalWidth, currentPageIndex, pageOrder, pages.length]
|
||||
);
|
||||
const rightSidePagesCount =
|
||||
pageOrder === PageOrder.LeftToRight ? pages.length - 1 - currentPageIndex : currentPageIndex;
|
||||
|
||||
const rightSidePagesWidth = useMemo(
|
||||
() =>
|
||||
`${
|
||||
pageOrder === PageOrder.LeftToRight
|
||||
? ajustedSidepagesTotalWidth -
|
||||
(currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
: (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
}vmin`,
|
||||
[ajustedSidepagesTotalWidth, currentPageIndex, pageOrder, pages.length]
|
||||
);
|
||||
const leftSidePagesWidth = `${
|
||||
pageOrder === PageOrder.LeftToRight
|
||||
? (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
: ajustedSidepagesTotalWidth - (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
}vmin`;
|
||||
|
||||
const leftSideClipPath = useMemo(
|
||||
() => `polygon(
|
||||
const rightSidePagesWidth = `${
|
||||
pageOrder === PageOrder.LeftToRight
|
||||
? ajustedSidepagesTotalWidth - (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
: (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
|
||||
}vmin`;
|
||||
|
||||
const leftSideClipPath = `polygon(
|
||||
${
|
||||
isSidePagesEnabled
|
||||
? `
|
||||
|
@ -265,12 +239,9 @@ const LibrarySlug = ({
|
|||
: "101% 0%, 101% 100%,"
|
||||
}
|
||||
70% 100%
|
||||
)`,
|
||||
[filterSettings.bookFold, isSidePagesEnabled, leftSidePagesWidth]
|
||||
);
|
||||
)`;
|
||||
|
||||
const rightSideClipPath = useMemo(
|
||||
() => `polygon(
|
||||
const rightSideClipPath = `polygon(
|
||||
${
|
||||
isSidePagesEnabled
|
||||
? `calc(100% - ${rightSidePagesWidth}) 0%,
|
||||
|
@ -295,48 +266,237 @@ const LibrarySlug = ({
|
|||
: "-1% 100%, -1% 0%,"
|
||||
}
|
||||
30% 0%
|
||||
)`,
|
||||
[filterSettings.bookFold, isSidePagesEnabled, rightSidePagesWidth]
|
||||
);
|
||||
)`;
|
||||
|
||||
const pageHeight = useMemo(
|
||||
() => `calc(100vh - ${is1ColumnLayout ? 5 : 4}rem - 3rem)`,
|
||||
[is1ColumnLayout]
|
||||
);
|
||||
const pageHeight = `calc(100vh - ${is1ColumnLayout ? 5 : 4}rem - 3rem)`;
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton title={langui.item} href={`/library/${itemSlug}`} />
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton title={langui.item} href={`/library/${itemSlug}`} />
|
||||
|
||||
<div className="mt-4 grid gap-2">
|
||||
<WithLabel label={langui.paper_texture}>
|
||||
<Switch value={filterSettings.paperTexture} onClick={togglePaperTexture} />
|
||||
</WithLabel>
|
||||
<div className="mt-4 grid gap-2">
|
||||
<WithLabel label={langui.paper_texture}>
|
||||
<Switch value={filterSettings.paperTexture} onClick={togglePaperTexture} />
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.book_fold}>
|
||||
<Switch value={filterSettings.bookFold} onClick={toggleBookFold} />
|
||||
</WithLabel>
|
||||
<WithLabel label={langui.book_fold}>
|
||||
<Switch value={filterSettings.bookFold} onClick={toggleBookFold} />
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.lighting}>
|
||||
<Switch value={filterSettings.lighting} onClick={toggleLighting} />
|
||||
</WithLabel>
|
||||
<WithLabel label={langui.lighting}>
|
||||
<Switch value={filterSettings.lighting} onClick={toggleLighting} />
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.side_pages}>
|
||||
<Switch value={isSidePagesEnabled} onClick={toggleIsSidePagesEnabled} />
|
||||
</WithLabel>
|
||||
<WithLabel label={langui.side_pages}>
|
||||
<Switch value={isSidePagesEnabled} onClick={toggleIsSidePagesEnabled} />
|
||||
</WithLabel>
|
||||
|
||||
{!isWebkit && (
|
||||
<WithLabel label={langui.shadow}>
|
||||
<Switch value={filterSettings.dropShadow} onClick={toggleDropShadow} />
|
||||
</WithLabel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid">
|
||||
<p>{langui.night_reader}:</p>
|
||||
<div className="mt-4 grid">
|
||||
<p>{langui.night_reader}:</p>
|
||||
<Slider
|
||||
min={0}
|
||||
max={10}
|
||||
value={filterSettings.teint * 10}
|
||||
onChange={(event) => {
|
||||
let value = 0;
|
||||
if (Array.isArray(event)) {
|
||||
value = event[0];
|
||||
} else {
|
||||
value = event;
|
||||
}
|
||||
setTeint(value / 10);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-2">
|
||||
<p>{langui.reading_layout}:</p>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
icon: Icon.Description,
|
||||
tooltip: langui.single_page_view,
|
||||
active: displayMode === "single",
|
||||
onClick: () => changeDisplayMode("single"),
|
||||
},
|
||||
{
|
||||
icon: Icon.AutoStories,
|
||||
tooltip: langui.double_page_view,
|
||||
active: displayMode === "double",
|
||||
onClick: () => changeDisplayMode("double"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-2">
|
||||
<p>{langui.quality}:</p>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
text: "SD",
|
||||
active: pageQuality === ImageQuality.Medium,
|
||||
onClick: () => setPageQuality(ImageQuality.Medium),
|
||||
},
|
||||
{
|
||||
text: "HD",
|
||||
active: pageQuality === ImageQuality.Large,
|
||||
onClick: () => setPageQuality(ImageQuality.Large),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_options}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
resetReaderSettings();
|
||||
setDisplayMode(is1ColumnLayout ? "single" : "double");
|
||||
sendAnalytics("Reader", "Reset all options");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full} className="grid place-content-center !p-0">
|
||||
<div className={cJoin("mb-12 grid", cIf(is1ColumnLayout, "!p-0", "!p-8"))}>
|
||||
<TransformWrapper
|
||||
onZoom={(zoom) => setCurrentZoom(zoom.state.scale)}
|
||||
panning={{ disabled: currentZoom <= 1, velocityDisabled: false }}
|
||||
doubleClick={{ disabled: true, mode: "reset" }}
|
||||
zoomAnimation={{ size: 0.1 }}
|
||||
velocityAnimation={{ animationTime: 0, equalToMove: true }}>
|
||||
<TransformComponent
|
||||
wrapperStyle={{ overflow: "visible", placeSelf: "center" }}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
gridAutoFlow: "column",
|
||||
display: "grid",
|
||||
placeContent: "center",
|
||||
filter:
|
||||
!filterSettings.dropShadow || isWebkit
|
||||
? undefined
|
||||
: isDarkMode
|
||||
? CUSTOM_DARK_DROPSHADOW
|
||||
: CUSTOM_LIGHT_DROPSHADOW,
|
||||
}}>
|
||||
{effectiveDisplayMode === "single" ? (
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative grid grid-flow-col",
|
||||
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
|
||||
)}>
|
||||
<Img
|
||||
style={{ maxHeight: pageHeight, width: "auto" }}
|
||||
src={firstPage}
|
||||
quality={pageQuality}
|
||||
/>
|
||||
<PageFilters page="single" bookType={bookType} options={filterSettings} />
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1/2"
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("left")}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 w-1/2"
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("right")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative grid grid-flow-col",
|
||||
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
|
||||
)}
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("left")}
|
||||
style={{
|
||||
clipPath: leftSideClipPath,
|
||||
}}>
|
||||
{isSidePagesEnabled && (
|
||||
<div
|
||||
style={{
|
||||
width: leftSidePagesWidth,
|
||||
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
|
||||
backgroundSize: `${
|
||||
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / leftSidePagesCount) * 100
|
||||
}% 100%`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Img
|
||||
style={{ maxHeight: pageHeight, width: "auto" }}
|
||||
src={pageOrder === PageOrder.LeftToRight ? firstPage : secondPage}
|
||||
quality={pageQuality}
|
||||
/>
|
||||
<PageFilters page="left" bookType={bookType} options={filterSettings} />
|
||||
</div>
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative grid grid-flow-col",
|
||||
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
|
||||
)}
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("right")}
|
||||
style={{
|
||||
clipPath: rightSideClipPath,
|
||||
}}>
|
||||
<Img
|
||||
style={{ maxHeight: pageHeight, width: "auto" }}
|
||||
className={cIf(
|
||||
is1ColumnLayout,
|
||||
`max-h-[calc(100vh-5rem)]`,
|
||||
"max-h-[calc(100vh-4rem)]"
|
||||
)}
|
||||
src={pageOrder === PageOrder.LeftToRight ? secondPage : firstPage}
|
||||
quality={pageQuality}
|
||||
/>
|
||||
{isSidePagesEnabled && (
|
||||
<div
|
||||
style={{
|
||||
width: rightSidePagesWidth,
|
||||
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
|
||||
backgroundPositionX: "right",
|
||||
backgroundSize: `${
|
||||
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / rightSidePagesCount) * 100
|
||||
}% 100%`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PageFilters page="right" bookType={bookType} options={filterSettings} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
</div>
|
||||
<div
|
||||
className={cJoin(
|
||||
`absolute inset-0 bg-light
|
||||
transition-transform duration-500`,
|
||||
cIf(isGalleryMode, "translate-y-0", "translate-y-[calc(100%-3rem)]")
|
||||
)}>
|
||||
<div
|
||||
className="mb-4 mt-3 grid grid-flow-col grid-cols-[auto,1fr,auto]
|
||||
place-content-center place-items-center gap-4 px-4">
|
||||
<p className="text-dark">
|
||||
{currentPageIndex - 1} / {pages.length - 2}
|
||||
</p>
|
||||
<Slider
|
||||
reverse={pageOrder === PageOrder.RightToLeft}
|
||||
min={0}
|
||||
max={10}
|
||||
value={filterSettings.teint * 10}
|
||||
max={pages.length - 1}
|
||||
value={currentPageIndex - 1}
|
||||
onChange={(event) => {
|
||||
let value = 0;
|
||||
if (Array.isArray(event)) {
|
||||
|
@ -344,317 +504,60 @@ const LibrarySlug = ({
|
|||
} else {
|
||||
value = event;
|
||||
}
|
||||
setTeint(value / 10);
|
||||
changeCurrentPageIndex(() => value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-2">
|
||||
<p>{langui.reading_layout}:</p>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
icon: Icon.Description,
|
||||
tooltip: langui.single_page_view,
|
||||
active: displayMode === "single",
|
||||
onClick: () => changeDisplayMode("single"),
|
||||
},
|
||||
{
|
||||
icon: Icon.AutoStories,
|
||||
tooltip: langui.double_page_view,
|
||||
active: displayMode === "double",
|
||||
onClick: () => changeDisplayMode("double"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-2">
|
||||
<p>{langui.quality}:</p>
|
||||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
text: "SD",
|
||||
active: pageQuality === ImageQuality.Medium,
|
||||
onClick: () => setPageQuality(ImageQuality.Medium),
|
||||
},
|
||||
{
|
||||
text: "HD",
|
||||
active: pageQuality === ImageQuality.Large,
|
||||
onClick: () => setPageQuality(ImageQuality.Large),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_options}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
resetReaderSettings();
|
||||
setDisplayMode(is1ColumnLayout ? "single" : "double");
|
||||
sendAnalytics("Reader", "Reset all options");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
langui.item,
|
||||
langui.paper_texture,
|
||||
langui.book_fold,
|
||||
langui.lighting,
|
||||
langui.side_pages,
|
||||
langui.shadow,
|
||||
langui.night_reader,
|
||||
langui.reading_layout,
|
||||
langui.single_page_view,
|
||||
langui.double_page_view,
|
||||
langui.quality,
|
||||
langui.reset_all_options,
|
||||
itemSlug,
|
||||
filterSettings.paperTexture,
|
||||
filterSettings.bookFold,
|
||||
filterSettings.lighting,
|
||||
filterSettings.dropShadow,
|
||||
filterSettings.teint,
|
||||
togglePaperTexture,
|
||||
toggleBookFold,
|
||||
toggleLighting,
|
||||
isSidePagesEnabled,
|
||||
toggleIsSidePagesEnabled,
|
||||
toggleDropShadow,
|
||||
displayMode,
|
||||
pageQuality,
|
||||
setTeint,
|
||||
changeDisplayMode,
|
||||
setPageQuality,
|
||||
resetReaderSettings,
|
||||
is1ColumnLayout,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full} className="grid place-content-center !p-0">
|
||||
<div className={cJoin("mb-12 grid", cIf(is1ColumnLayout, "!p-0", "!p-8"))}>
|
||||
<TransformWrapper
|
||||
onZoom={(zoom) => setCurrentZoom(zoom.state.scale)}
|
||||
panning={{ disabled: currentZoom <= 1, velocityDisabled: false }}
|
||||
doubleClick={{ disabled: true, mode: "reset" }}
|
||||
zoomAnimation={{ size: 0.1 }}
|
||||
velocityAnimation={{ animationTime: 0, equalToMove: true }}>
|
||||
<TransformComponent
|
||||
wrapperStyle={{ overflow: "visible", placeSelf: "center" }}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
gridAutoFlow: "column",
|
||||
display: "grid",
|
||||
placeContent: "center",
|
||||
filter: filterSettings.dropShadow
|
||||
? isDarkMode
|
||||
? CUSTOM_DARK_DROPSHADOW
|
||||
: CUSTOM_LIGHT_DROPSHADOW
|
||||
: undefined,
|
||||
}}>
|
||||
{effectiveDisplayMode === "single" ? (
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative grid grid-flow-col",
|
||||
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
|
||||
)}>
|
||||
<Img
|
||||
style={{ maxHeight: pageHeight, width: "auto" }}
|
||||
src={firstPage}
|
||||
quality={pageQuality}
|
||||
/>
|
||||
<PageFilters page="single" bookType={bookType} options={filterSettings} />
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1/2"
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("left")}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 w-1/2"
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("right")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative grid grid-flow-col",
|
||||
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
|
||||
)}
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("left")}
|
||||
style={{
|
||||
clipPath: leftSideClipPath,
|
||||
}}>
|
||||
{isSidePagesEnabled && (
|
||||
<div
|
||||
style={{
|
||||
width: leftSidePagesWidth,
|
||||
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
|
||||
backgroundSize: `${
|
||||
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / leftSidePagesCount) * 100
|
||||
}% 100%`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Img
|
||||
style={{ maxHeight: pageHeight, width: "auto" }}
|
||||
src={pageOrder === PageOrder.LeftToRight ? firstPage : secondPage}
|
||||
quality={pageQuality}
|
||||
/>
|
||||
<PageFilters page="left" bookType={bookType} options={filterSettings} />
|
||||
</div>
|
||||
<div
|
||||
className={cJoin(
|
||||
"relative grid grid-flow-col",
|
||||
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
|
||||
)}
|
||||
onClick={() => currentZoom <= 1 && handlePageNavigation("right")}
|
||||
style={{
|
||||
clipPath: rightSideClipPath,
|
||||
}}>
|
||||
<Img
|
||||
style={{ maxHeight: pageHeight, width: "auto" }}
|
||||
className={cIf(
|
||||
is1ColumnLayout,
|
||||
`max-h-[calc(100vh-5rem)]`,
|
||||
"max-h-[calc(100vh-4rem)]"
|
||||
)}
|
||||
src={pageOrder === PageOrder.LeftToRight ? secondPage : firstPage}
|
||||
quality={pageQuality}
|
||||
/>
|
||||
{isSidePagesEnabled && (
|
||||
<div
|
||||
style={{
|
||||
width: rightSidePagesWidth,
|
||||
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
|
||||
backgroundPositionX: "right",
|
||||
backgroundSize: `${
|
||||
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / rightSidePagesCount) * 100
|
||||
}% 100%`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PageFilters page="right" bookType={bookType} options={filterSettings} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
</div>
|
||||
<div
|
||||
className={cJoin(
|
||||
`absolute inset-0 bg-light
|
||||
transition-transform duration-500`,
|
||||
cIf(isGalleryMode, "translate-y-0", "translate-y-[calc(100%-3rem)]")
|
||||
)}>
|
||||
<div
|
||||
className="mb-4 mt-3 grid grid-flow-col grid-cols-[auto,1fr,auto]
|
||||
place-content-center place-items-center gap-4 px-4">
|
||||
<p className="text-dark">
|
||||
{currentPageIndex - 1} / {pages.length - 2}
|
||||
</p>
|
||||
<Slider
|
||||
reverse={pageOrder === PageOrder.RightToLeft}
|
||||
min={0}
|
||||
max={pages.length - 1}
|
||||
value={currentPageIndex - 1}
|
||||
onChange={(event) => {
|
||||
let value = 0;
|
||||
if (Array.isArray(event)) {
|
||||
value = event[0];
|
||||
} else {
|
||||
value = event;
|
||||
}
|
||||
changeCurrentPageIndex(() => value);
|
||||
}}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon={isGalleryMode ? Icon.ExpandMore : Icon.ExpandLess}
|
||||
onClick={() => setIsGalleryMode((current) => !current)}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
icon={isFullscreen ? Icon.FullscreenExit : Icon.Fullscreen}
|
||||
onClick={toggleFullscreen}
|
||||
size="small"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon={isGalleryMode ? Icon.ExpandMore : Icon.ExpandLess}
|
||||
onClick={() => setIsGalleryMode((current) => !current)}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
icon={isFullscreen ? Icon.FullscreenExit : Icon.Fullscreen}
|
||||
onClick={toggleFullscreen}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[calc(100vh-4rem)] overflow-y-scroll px-8">
|
||||
{item.contents?.data.map((content) => (
|
||||
<Fragment key={content.id}>
|
||||
{content.attributes?.scan_set?.[0] && (
|
||||
<TranslatedScanSet
|
||||
scanSet={content.attributes.scan_set}
|
||||
onClickOnImage={(index) => {
|
||||
const range = content.attributes?.range[0];
|
||||
let newPageIndex = index + 1;
|
||||
if (range?.__typename === "ComponentRangePageRange") {
|
||||
newPageIndex += range.starting_page;
|
||||
}
|
||||
changeCurrentPageIndex(() => newPageIndex);
|
||||
setIsGalleryMode(false);
|
||||
}}
|
||||
id={content.attributes.slug}
|
||||
translations={filterHasAttributes(
|
||||
content.attributes.content?.data?.attributes?.translations,
|
||||
["language.data.attributes"] as const
|
||||
).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
),
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(content.attributes.slug, item.slug),
|
||||
}}
|
||||
content={content.attributes.content}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
is1ColumnLayout,
|
||||
currentZoom,
|
||||
filterSettings,
|
||||
isDarkMode,
|
||||
pageHeight,
|
||||
effectiveDisplayMode,
|
||||
firstPage,
|
||||
pageQuality,
|
||||
bookType,
|
||||
leftSideClipPath,
|
||||
isSidePagesEnabled,
|
||||
leftSidePagesWidth,
|
||||
leftSidePagesCount,
|
||||
pageOrder,
|
||||
secondPage,
|
||||
rightSideClipPath,
|
||||
rightSidePagesWidth,
|
||||
rightSidePagesCount,
|
||||
isGalleryMode,
|
||||
currentPageIndex,
|
||||
pages.length,
|
||||
isFullscreen,
|
||||
toggleFullscreen,
|
||||
item.contents?.data,
|
||||
item.slug,
|
||||
handlePageNavigation,
|
||||
changeCurrentPageIndex,
|
||||
]
|
||||
<div className="h-[calc(100vh-4rem)] overflow-y-scroll px-8">
|
||||
{item.contents?.data.map((content) => (
|
||||
<Fragment key={content.id}>
|
||||
{content.attributes?.scan_set?.[0] && (
|
||||
<TranslatedScanSet
|
||||
scanSet={content.attributes.scan_set}
|
||||
onClickOnImage={(index) => {
|
||||
const range = content.attributes?.range[0];
|
||||
let newPageIndex = index + 1;
|
||||
if (range?.__typename === "ComponentRangePageRange") {
|
||||
newPageIndex += range.starting_page;
|
||||
}
|
||||
changeCurrentPageIndex(() => newPageIndex);
|
||||
setIsGalleryMode(false);
|
||||
}}
|
||||
id={content.attributes.slug}
|
||||
translations={filterHasAttributes(
|
||||
content.attributes.content?.data?.attributes?.translations,
|
||||
["language.data.attributes"] as const
|
||||
).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
),
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(content.attributes.slug, item.slug),
|
||||
}}
|
||||
content={content.attributes.content}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -798,9 +701,9 @@ interface PageFiltersProps {
|
|||
|
||||
const PageFilters = ({ page, bookType, options }: PageFiltersProps) => {
|
||||
const isDarkMode = useAtomGetter(atoms.settings.darkMode);
|
||||
const commonCss = useMemo(
|
||||
() => cJoin("absolute inset-0", cIf(page === "right", "[background-position-x:-100%]")),
|
||||
[page]
|
||||
const commonCss = cJoin(
|
||||
"absolute inset-0",
|
||||
cIf(page === "right", "[background-position-x:-100%]")
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -929,10 +832,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
|
|||
}, []),
|
||||
});
|
||||
|
||||
const pages = useMemo(
|
||||
() => filterHasAttributes(selectedScan?.pages?.data, ["attributes"]),
|
||||
[selectedScan]
|
||||
);
|
||||
const pages = filterHasAttributes(selectedScan?.pages?.data, ["attributes"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
|
@ -222,255 +222,222 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
[groupingMethod, langui]
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.LibraryBooks}
|
||||
title={langui.library}
|
||||
description={langui.library_description}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.LibraryBooks}
|
||||
title={langui.library}
|
||||
description={langui.library_description}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Library", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Library", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Library", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Library", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.category ?? "Category",
|
||||
langui.type ?? "Type",
|
||||
langui.release_year ?? "Year",
|
||||
]}
|
||||
value={groupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
sendAnalytics(
|
||||
"Library",
|
||||
`Change grouping method (${["none", "category", "type", "year"][value + 1]})`
|
||||
);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.order_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.name ?? "Name",
|
||||
langui.price ?? "Price",
|
||||
langui.release_date ?? "Release date",
|
||||
]}
|
||||
value={sortingMethod}
|
||||
onChange={(value) => {
|
||||
setSortingMethod(value);
|
||||
sendAnalytics(
|
||||
"Library",
|
||||
`Change sorting method (${["name", "price", "release date"][value]})`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_subitems}>
|
||||
<Switch
|
||||
value={showSubitems}
|
||||
onClick={() => {
|
||||
toggleShowSubitems();
|
||||
sendAnalytics("Library", `${showSubitems ? "Hide" : "Show"} subitems`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_primary_items}>
|
||||
<Switch
|
||||
value={showPrimaryItems}
|
||||
onClick={() => {
|
||||
toggleShowPrimaryItems();
|
||||
sendAnalytics("Library", `${showPrimaryItems ? "Hide" : "Show"} primary items`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_secondary_items}>
|
||||
<Switch
|
||||
value={showSecondaryItems}
|
||||
onClick={() => {
|
||||
toggleShowSecondaryItems();
|
||||
sendAnalytics("Library", `${showSecondaryItems ? "Hide" : "Show"} secondary items`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("Library", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
<ButtonGroup
|
||||
className="mt-4"
|
||||
buttonsProps={[
|
||||
{
|
||||
tooltip: langui.only_display_items_i_want,
|
||||
icon: Icon.Favorite,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.Want);
|
||||
sendAnalytics("Library", "Set filter status (I want)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.Want,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_items_i_have,
|
||||
icon: Icon.BackHand,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.Have);
|
||||
sendAnalytics("Library", "Set filter status (I have)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.Have,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_unmarked_items,
|
||||
icon: Icon.RadioButtonUnchecked,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.None);
|
||||
sendAnalytics("Library", "Set filter status (unmarked)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.None,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_unmarked_items,
|
||||
text: langui.all,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(undefined);
|
||||
sendAnalytics("Library", "Set filter status (all)");
|
||||
},
|
||||
active: isUndefined(filterUserStatus),
|
||||
},
|
||||
<WithLabel label={langui.group_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.category ?? "Category",
|
||||
langui.type ?? "Type",
|
||||
langui.release_year ?? "Year",
|
||||
]}
|
||||
value={groupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
sendAnalytics(
|
||||
"Library",
|
||||
`Change grouping method (${["none", "category", "type", "year"][value + 1]})`
|
||||
);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setShowSubitems(DEFAULT_FILTERS_STATE.showSubitems);
|
||||
setShowPrimaryItems(DEFAULT_FILTERS_STATE.showPrimaryItems);
|
||||
setShowSecondaryItems(DEFAULT_FILTERS_STATE.showSecondaryItems);
|
||||
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
setFilterUserStatus(DEFAULT_FILTERS_STATE.filterUserStatus);
|
||||
sendAnalytics("Library", "Reset all filters");
|
||||
<WithLabel label={langui.order_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.name ?? "Name",
|
||||
langui.price ?? "Price",
|
||||
langui.release_date ?? "Release date",
|
||||
]}
|
||||
value={sortingMethod}
|
||||
onChange={(value) => {
|
||||
setSortingMethod(value);
|
||||
sendAnalytics(
|
||||
"Library",
|
||||
`Change sorting method (${["name", "price", "release date"][value]})`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
filterUserStatus,
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
searchName,
|
||||
setKeepInfoVisible,
|
||||
setShowPrimaryItems,
|
||||
setShowSecondaryItems,
|
||||
setShowSubitems,
|
||||
showPrimaryItems,
|
||||
showSecondaryItems,
|
||||
showSubitems,
|
||||
sortingMethod,
|
||||
toggleKeepInfoVisible,
|
||||
toggleShowPrimaryItems,
|
||||
toggleShowSecondaryItems,
|
||||
toggleShowSubitems,
|
||||
]
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_subitems}>
|
||||
<Switch
|
||||
value={showSubitems}
|
||||
onClick={() => {
|
||||
toggleShowSubitems();
|
||||
sendAnalytics("Library", `${showSubitems ? "Hide" : "Show"} subitems`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_primary_items}>
|
||||
<Switch
|
||||
value={showPrimaryItems}
|
||||
onClick={() => {
|
||||
toggleShowPrimaryItems();
|
||||
sendAnalytics("Library", `${showPrimaryItems ? "Hide" : "Show"} primary items`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_secondary_items}>
|
||||
<Switch
|
||||
value={showSecondaryItems}
|
||||
onClick={() => {
|
||||
toggleShowSecondaryItems();
|
||||
sendAnalytics("Library", `${showSecondaryItems ? "Hide" : "Show"} secondary items`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("Library", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
<ButtonGroup
|
||||
className="mt-4"
|
||||
buttonsProps={[
|
||||
{
|
||||
tooltip: langui.only_display_items_i_want,
|
||||
icon: Icon.Favorite,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.Want);
|
||||
sendAnalytics("Library", "Set filter status (I want)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.Want,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_items_i_have,
|
||||
icon: Icon.BackHand,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.Have);
|
||||
sendAnalytics("Library", "Set filter status (I have)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.Have,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_unmarked_items,
|
||||
icon: Icon.RadioButtonUnchecked,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.None);
|
||||
sendAnalytics("Library", "Set filter status (unmarked)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.None,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_unmarked_items,
|
||||
text: langui.all,
|
||||
onClick: () => {
|
||||
setFilterUserStatus(undefined);
|
||||
sendAnalytics("Library", "Set filter status (all)");
|
||||
},
|
||||
active: isUndefined(filterUserStatus),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setShowSubitems(DEFAULT_FILTERS_STATE.showSubitems);
|
||||
setShowPrimaryItems(DEFAULT_FILTERS_STATE.showPrimaryItems);
|
||||
setShowSecondaryItems(DEFAULT_FILTERS_STATE.showSecondaryItems);
|
||||
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
setFilterUserStatus(DEFAULT_FILTERS_STATE.filterUserStatus);
|
||||
sendAnalytics("Library", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(items, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<PreviewCard
|
||||
href={`/library/${item.attributes.slug}`}
|
||||
title={item.attributes.title}
|
||||
subtitle={item.attributes.subtitle}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
item.attributes.metadata &&
|
||||
item.attributes.metadata.length > 0 &&
|
||||
item.attributes.metadata[0]
|
||||
? [prettyItemSubType(item.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.release_date,
|
||||
price: item.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(item.attributes.metadata?.[0]) && (
|
||||
<PreviewCardCTAs id={item.id} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"grid-cols-2 items-end",
|
||||
cIf(isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]")
|
||||
)}
|
||||
searchingTerm={searchName}
|
||||
sortingFunction={sortingFunction}
|
||||
groupingFunction={groupingFunction}
|
||||
searchingBy={(item) =>
|
||||
prettyInlineTitle("", item.attributes.title, item.attributes.subtitle)
|
||||
}
|
||||
filteringFunction={filteringFunction}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
filteringFunction,
|
||||
groupingFunction,
|
||||
isContentPanelAtLeast4xl,
|
||||
items,
|
||||
keepInfoVisible,
|
||||
searchName,
|
||||
sortingFunction,
|
||||
]
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(items, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<PreviewCard
|
||||
href={`/library/${item.attributes.slug}`}
|
||||
title={item.attributes.title}
|
||||
subtitle={item.attributes.subtitle}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
item.attributes.metadata &&
|
||||
item.attributes.metadata.length > 0 &&
|
||||
item.attributes.metadata[0]
|
||||
? [prettyItemSubType(item.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.release_date,
|
||||
price: item.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(item.attributes.metadata?.[0]) && (
|
||||
<PreviewCardCTAs id={item.id} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"grid-cols-2 items-end",
|
||||
cIf(isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]")
|
||||
)}
|
||||
searchingTerm={searchName}
|
||||
sortingFunction={sortingFunction}
|
||||
groupingFunction={groupingFunction}
|
||||
searchingBy={(item) =>
|
||||
prettyInlineTitle("", item.attributes.title, item.attributes.subtitle)
|
||||
}
|
||||
filteringFunction={filteringFunction}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
|
@ -58,101 +58,95 @@ const News = ({ posts, ...otherProps }: Props): JSX.Element => {
|
|||
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader icon={Icon.Feed} title={langui.news} description={langui.news_description} />
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader icon={Icon.Feed} title={langui.news} description={langui.news_description} />
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("News", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("News", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("News", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("News", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("News", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("News", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("News", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui, searchName, setKeepInfoVisible, toggleKeepInfoVisible]
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("News", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(posts, ["attributes", "id"] as const)}
|
||||
getItemId={(post) => post.id}
|
||||
renderItem={({ item: post }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/news/${post.attributes.slug}`}
|
||||
translations={filterHasAttributes(post.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: translation.title,
|
||||
description: translation.excerpt,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(post.attributes.slug) }}
|
||||
thumbnail={post.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
bottomChips={post.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: post.attributes.date,
|
||||
releaseDateFormat: "long",
|
||||
position: "Top",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-4 gap-y-6"
|
||||
)}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(post) =>
|
||||
`${prettySlug(post.attributes.slug)} ${post.attributes.translations
|
||||
?.map((translation) => translation?.title)
|
||||
.join(" ")}`
|
||||
}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[keepInfoVisible, posts, searchName, isContentPanelAtLeast4xl]
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(posts, ["attributes", "id"] as const)}
|
||||
getItemId={(post) => post.id}
|
||||
renderItem={({ item: post }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/news/${post.attributes.slug}`}
|
||||
translations={filterHasAttributes(post.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: translation.title,
|
||||
description: translation.excerpt,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(post.attributes.slug) }}
|
||||
thumbnail={post.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
bottomChips={post.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: post.attributes.date,
|
||||
releaseDateFormat: "long",
|
||||
position: "Top",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-4 gap-y-6"
|
||||
)}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(post) =>
|
||||
`${prettySlug(post.attributes.slug)} ${post.attributes.translations
|
||||
?.map((translation) => translation?.title)
|
||||
.join(" ")}`
|
||||
}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
if (isTerminalMode) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
|
@ -49,146 +49,126 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
|
|||
});
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton href={`/wiki`} title={langui.wiki} displayOnlyOn={"3ColumnsLayout"} />
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton href={`/wiki`} title={langui.wiki} displayOnlyOn={"3ColumnsLayout"} />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Large}>
|
||||
<ReturnButton
|
||||
href={`/wiki`}
|
||||
title={langui.wiki}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Large}>
|
||||
<ReturnButton
|
||||
href={`/wiki`}
|
||||
title={langui.wiki}
|
||||
displayOnlyOn={"1ColumnLayout"}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap place-content-center gap-3">
|
||||
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1>
|
||||
{selectedTranslation?.aliases && selectedTranslation.aliases.length > 0 && (
|
||||
<p className="mr-3 text-center text-2xl">
|
||||
{`(${selectedTranslation.aliases.map((alias) => alias?.alias).join("・")})`}
|
||||
</p>
|
||||
)}
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
</div>
|
||||
|
||||
{selectedTranslation && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div className="text-justify">
|
||||
<div
|
||||
className={cJoin(
|
||||
"mb-8 overflow-hidden rounded-lg bg-mid text-center",
|
||||
cIf(is3ColumnsLayout, "float-right ml-8 w-96")
|
||||
)}>
|
||||
{page.thumbnail?.data?.attributes && (
|
||||
<Img
|
||||
src={page.thumbnail.data.attributes}
|
||||
quality={ImageQuality.Medium}
|
||||
className="w-full cursor-pointer"
|
||||
onClick={() => {
|
||||
if (page.thumbnail?.data?.attributes) {
|
||||
showLightBox([page.thumbnail.data.attributes]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="my-4 grid gap-4 p-4">
|
||||
{page.categories?.data && page.categories.data.length > 0 && (
|
||||
<>
|
||||
<p className="font-headers text-xl font-bold">{langui.categories}</p>
|
||||
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(page.categories.data, ["attributes"] as const).map(
|
||||
(category) => (
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{page.tags?.data && page.tags.data.length > 0 && (
|
||||
<>
|
||||
<p className="font-headers text-xl font-bold">{langui.tags}</p>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(page.tags.data, ["attributes"] as const).map((tag) => (
|
||||
<Chip
|
||||
key={tag.id}
|
||||
text={
|
||||
tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.summary) && (
|
||||
<div className="mb-12">
|
||||
<p className="font-headers text-lg font-bold">{langui.summary}</p>
|
||||
<p>{selectedTranslation.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterHasAttributes(page.definitions, ["translations"] as const).map(
|
||||
(definition, index) => (
|
||||
<div key={index} className="mb-12">
|
||||
<DefinitionCard
|
||||
source={{
|
||||
name: definition.source?.data?.attributes?.name,
|
||||
url: definition.source?.data?.attributes?.content?.data?.attributes?.slug
|
||||
? sJoin(
|
||||
"/contents/",
|
||||
definition.source.data.attributes.content.data.attributes.slug
|
||||
)
|
||||
: cJoin(
|
||||
"/library/",
|
||||
definition.source?.data?.attributes?.ranged_content?.data?.attributes
|
||||
?.library_item?.data?.attributes?.slug
|
||||
),
|
||||
}}
|
||||
translations={definition.translations.map((translation) => ({
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
definition: translation?.definition,
|
||||
status: translation?.status,
|
||||
}))}
|
||||
index={index + 1}
|
||||
categories={filterHasAttributes(definition.categories?.data, [
|
||||
"attributes",
|
||||
] as const).map((category) => category.attributes.short)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-wrap place-content-center gap-3">
|
||||
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1>
|
||||
{selectedTranslation?.aliases && selectedTranslation.aliases.length > 0 && (
|
||||
<p className="mr-3 text-center text-2xl">
|
||||
{`(${selectedTranslation.aliases.map((alias) => alias?.alias).join("・")})`}
|
||||
</p>
|
||||
)}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
is3ColumnsLayout,
|
||||
languageSwitcherProps,
|
||||
langui.categories,
|
||||
langui.summary,
|
||||
langui.tags,
|
||||
langui.wiki,
|
||||
page.categories?.data,
|
||||
page.definitions,
|
||||
page.tags?.data,
|
||||
page.thumbnail?.data?.attributes,
|
||||
selectedTranslation,
|
||||
showLightBox,
|
||||
]
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
</div>
|
||||
|
||||
{selectedTranslation && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div className="text-justify">
|
||||
<div
|
||||
className={cJoin(
|
||||
"mb-8 overflow-hidden rounded-lg bg-mid text-center",
|
||||
cIf(is3ColumnsLayout, "float-right ml-8 w-96")
|
||||
)}>
|
||||
{page.thumbnail?.data?.attributes && (
|
||||
<Img
|
||||
src={page.thumbnail.data.attributes}
|
||||
quality={ImageQuality.Medium}
|
||||
className="w-full cursor-pointer"
|
||||
onClick={() => {
|
||||
if (page.thumbnail?.data?.attributes) {
|
||||
showLightBox([page.thumbnail.data.attributes]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="my-4 grid gap-4 p-4">
|
||||
{page.categories?.data && page.categories.data.length > 0 && (
|
||||
<>
|
||||
<p className="font-headers text-xl font-bold">{langui.categories}</p>
|
||||
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(page.categories.data, ["attributes"] as const).map(
|
||||
(category) => (
|
||||
<Chip key={category.id} text={category.attributes.name} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{page.tags?.data && page.tags.data.length > 0 && (
|
||||
<>
|
||||
<p className="font-headers text-xl font-bold">{langui.tags}</p>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{filterHasAttributes(page.tags.data, ["attributes"] as const).map((tag) => (
|
||||
<Chip
|
||||
key={tag.id}
|
||||
text={
|
||||
tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.summary) && (
|
||||
<div className="mb-12">
|
||||
<p className="font-headers text-lg font-bold">{langui.summary}</p>
|
||||
<p>{selectedTranslation.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterHasAttributes(page.definitions, ["translations"] as const).map(
|
||||
(definition, index) => (
|
||||
<div key={index} className="mb-12">
|
||||
<DefinitionCard
|
||||
source={{
|
||||
name: definition.source?.data?.attributes?.name,
|
||||
url: definition.source?.data?.attributes?.content?.data?.attributes?.slug
|
||||
? sJoin(
|
||||
"/contents/",
|
||||
definition.source.data.attributes.content.data.attributes.slug
|
||||
)
|
||||
: cJoin(
|
||||
"/library/",
|
||||
definition.source?.data?.attributes?.ranged_content?.data?.attributes
|
||||
?.library_item?.data?.attributes?.slug
|
||||
),
|
||||
}}
|
||||
translations={definition.translations.map((translation) => ({
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
definition: translation?.definition,
|
||||
status: translation?.status,
|
||||
}))}
|
||||
index={index + 1}
|
||||
categories={filterHasAttributes(definition.categories?.data, [
|
||||
"attributes",
|
||||
] as const).map((category) => category.attributes.short)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
if (isTerminalMode) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { InsetBox } from "components/Containers/InsetBox";
|
||||
|
@ -46,80 +46,70 @@ interface Props extends AppLayoutRequired {
|
|||
|
||||
const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props): JSX.Element => {
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const ids = useMemo(
|
||||
() =>
|
||||
filterHasAttributes(chronologyEras, ["attributes"] as const).map(
|
||||
(era) => era.attributes.slug
|
||||
),
|
||||
[chronologyEras]
|
||||
const ids = filterHasAttributes(chronologyEras, ["attributes"] as const).map(
|
||||
(era) => era.attributes.slug
|
||||
);
|
||||
|
||||
const currentIntersection = useIntersectionList(ids);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton href="/wiki" title={langui.wiki} displayOnlyOn="3ColumnsLayout" />
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton href="/wiki" title={langui.wiki} displayOnlyOn="3ColumnsLayout" />
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map((era, index) => (
|
||||
<Fragment key={era.id}>
|
||||
<TranslatedNavOption
|
||||
translations={filterHasAttributes(era.attributes.title, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: translation.title,
|
||||
subtitle: `${era.attributes.starting_year} → ${era.attributes.ending_year}`,
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(era.attributes.slug),
|
||||
subtitle: `${era.attributes.starting_year} → ${era.attributes.ending_year}`,
|
||||
}}
|
||||
url={`#${era.attributes.slug}`}
|
||||
border
|
||||
active={currentIntersection === index}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</SubPanel>
|
||||
),
|
||||
[chronologyEras, currentIntersection, langui]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href="/wiki"
|
||||
title={langui.wiki}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{filterHasAttributes(chronologyEras, ["attributes"] as const).map((era) => (
|
||||
<TranslatedChronologyEra
|
||||
key={era.attributes.slug}
|
||||
id={era.attributes.slug}
|
||||
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map((era, index) => (
|
||||
<Fragment key={era.id}>
|
||||
<TranslatedNavOption
|
||||
translations={filterHasAttributes(era.attributes.title, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: translation.title,
|
||||
description: translation.description,
|
||||
subtitle: `${era.attributes.starting_year} → ${era.attributes.ending_year}`,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(era.attributes.slug) }}
|
||||
chronologyItems={filterHasAttributes(chronologyItems, ["attributes"] as const).filter(
|
||||
(item) =>
|
||||
item.attributes.year >= era.attributes.starting_year &&
|
||||
item.attributes.year < era.attributes.ending_year
|
||||
)}
|
||||
fallback={{
|
||||
title: prettySlug(era.attributes.slug),
|
||||
subtitle: `${era.attributes.starting_year} → ${era.attributes.ending_year}`,
|
||||
}}
|
||||
url={`#${era.attributes.slug}`}
|
||||
border
|
||||
active={currentIntersection === index}
|
||||
/>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[chronologyEras, chronologyItems, langui]
|
||||
</Fragment>
|
||||
))}
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href="/wiki"
|
||||
title={langui.wiki}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{filterHasAttributes(chronologyEras, ["attributes"] as const).map((era) => (
|
||||
<TranslatedChronologyEra
|
||||
key={era.attributes.slug}
|
||||
id={era.attributes.slug}
|
||||
translations={filterHasAttributes(era.attributes.title, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: translation.title,
|
||||
description: translation.description,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(era.attributes.slug) }}
|
||||
chronologyItems={filterHasAttributes(chronologyItems, ["attributes"] as const).filter(
|
||||
(item) =>
|
||||
item.attributes.year >= era.attributes.starting_year &&
|
||||
item.attributes.year < era.attributes.ending_year
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
|
||||
|
@ -161,7 +151,7 @@ interface ChronologyEraProps {
|
|||
}
|
||||
|
||||
const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEraProps) => {
|
||||
const yearGroups = useMemo(() => {
|
||||
const yearGroups = (() => {
|
||||
const memo: Props["chronologyItems"][] = [];
|
||||
let currentYear = -Infinity;
|
||||
filterHasAttributes(chronologyItems, ["attributes"] as const).forEach((item) => {
|
||||
|
@ -173,7 +163,7 @@ const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEr
|
|||
}
|
||||
});
|
||||
return memo;
|
||||
}, [chronologyItems]);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div id={id}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { NavOption } from "components/PanelComponents/NavOption";
|
||||
|
@ -67,84 +67,73 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
|
|||
setValue: setKeepInfoVisible,
|
||||
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.TravelExplore}
|
||||
title={langui.wiki}
|
||||
description={langui.wiki_description}
|
||||
/>
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.TravelExplore}
|
||||
title={langui.wiki}
|
||||
description={langui.wiki_description}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Wiki", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Wiki", "Clear search term");
|
||||
}
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
sendAnalytics("Wiki", "Change search term");
|
||||
} else {
|
||||
sendAnalytics("Wiki", "Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "Category"]}
|
||||
value={groupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
sendAnalytics("Wiki", `Change grouping method (${["none", "category"][value + 1]})`);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "Category"]}
|
||||
value={groupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
sendAnalytics("Wiki", `Change grouping method (${["none", "category"][value + 1]})`);
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("Wiki", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
sendAnalytics("Wiki", `Always ${keepInfoVisible ? "hide" : "show"} info`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("Wiki", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
sendAnalytics("Wiki", "Reset all filters");
|
||||
}}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
|
||||
<HorizontalLine />
|
||||
<p className="mb-4 font-headers text-xl font-bold">{langui.special_pages}</p>
|
||||
|
||||
<p className="mb-4 font-headers text-xl font-bold">{langui.special_pages}</p>
|
||||
|
||||
<NavOption title={langui.chronology} url="/wiki/chronology" border />
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
searchName,
|
||||
setKeepInfoVisible,
|
||||
toggleKeepInfoVisible,
|
||||
]
|
||||
<NavOption title={langui.chronology} url="/wiki/chronology" border />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const groupingFunction = useCallback(
|
||||
|
@ -172,64 +161,59 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
|
|||
[groupingMethod, langui]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(pages, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/wiki/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
title: translation.title,
|
||||
subtitle:
|
||||
translation.aliases && translation.aliases.length > 0
|
||||
? translation.aliases.map((alias) => alias?.alias).join("・")
|
||||
: undefined,
|
||||
description: translation.summary,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio={"4/3"}
|
||||
thumbnailRounded
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={filterHasAttributes(item.attributes.tags?.data, [
|
||||
"attributes",
|
||||
] as const).map(
|
||||
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
|
||||
)}
|
||||
bottomChips={filterHasAttributes(item.attributes.categories?.data, [
|
||||
"attributes",
|
||||
] as const).map((category) => category.attributes.short)}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) =>
|
||||
filterDefined(item.attributes.translations)
|
||||
.map(
|
||||
(translation) =>
|
||||
`${translation.title} ${filterDefined(translation.aliases)
|
||||
.map((alias) => alias.alias)
|
||||
.join(" ")}`
|
||||
)
|
||||
.join(" ")
|
||||
}
|
||||
groupingFunction={groupingFunction}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[groupingFunction, keepInfoVisible, pages, searchName, isContentPanelAtLeast4xl]
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(pages, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/wiki/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
title: translation.title,
|
||||
subtitle:
|
||||
translation.aliases && translation.aliases.length > 0
|
||||
? translation.aliases.map((alias) => alias?.alias).join("・")
|
||||
: undefined,
|
||||
description: translation.summary,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio={"4/3"}
|
||||
thumbnailRounded
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={filterHasAttributes(item.attributes.tags?.data, ["attributes"] as const).map(
|
||||
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
|
||||
)}
|
||||
bottomChips={filterHasAttributes(item.attributes.categories?.data, [
|
||||
"attributes",
|
||||
] as const).map((category) => category.attributes.short)}
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) =>
|
||||
filterDefined(item.attributes.translations)
|
||||
.map(
|
||||
(translation) =>
|
||||
`${translation.title} ${filterDefined(translation.aliases)
|
||||
.map((alias) => alias.alias)
|
||||
.join(" ")}`
|
||||
)
|
||||
.join(" ")
|
||||
}
|
||||
groupingFunction={groupingFunction}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
);
|
||||
|
||||
if (isTerminalMode) {
|
||||
|
|
|
@ -249,6 +249,21 @@ module.exports = {
|
|||
});
|
||||
}),
|
||||
|
||||
/* Webkit fixes */
|
||||
plugin(({ addUtilities }) => {
|
||||
addUtilities({
|
||||
".webkit-fixes": {
|
||||
"*": {
|
||||
"--tw-drop-shadow": "unset !important",
|
||||
"--tw-shadow": "unset !important",
|
||||
},
|
||||
".texture-paper-dots": {
|
||||
backgroundImage: "unset !important",
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
/* Add support for break-wrods CSS attribute */
|
||||
plugin(({ addUtilities }) => {
|
||||
addUtilities({
|
||||
|
|
Loading…
Reference in New Issue