Put an end to my useMemo craze + fixed ios

This commit is contained in:
DrMint 2022-12-04 15:31:11 +01:00
parent c356679813
commit 6a1be38613
44 changed files with 3887 additions and 3297 deletions

View File

@ -25,7 +25,7 @@
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository) #### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
- Language: [TypeScript](https://www.typescriptlang.org/) - 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/) - Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
- Fetch the GraphQL schema from the GraphQL back-end endpoint - Fetch the GraphQL schema from the GraphQL back-end endpoint
- Read the operations and fragments stored as graphql files in the `src/graphql` folder - Read the operations and fragments stored as graphql files in the `src/graphql` folder

View File

@ -6,7 +6,6 @@ const locales = ["en", "es", "fr", "pt-br", "ja"];
/* @type {import('next').NextConfig} */ /* @type {import('next').NextConfig} */
module.exports = { module.exports = {
swcMinify: true,
reactStrictMode: true, reactStrictMode: true,
poweredByHeader: false, poweredByHeader: false,
i18n: { i18n: {

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import Head from "next/head"; import Head from "next/head";
import { useMemo } from "react";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { layout } from "../../design.config"; import { layout } from "../../design.config";
import { Ico, Icon } from "./Ico"; import { Ico, Icon } from "./Ico";
import { MainPanel } from "./Panels/MainPanel"; import { MainPanel } from "./Panels/MainPanel";
import { SafariPopup } from "./Panels/SafariPopup";
import { isDefined, isUndefined } from "helpers/others"; import { isDefined, isUndefined } from "helpers/others";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph"; import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph";
@ -77,10 +75,7 @@ export const AppLayout = ({
}, },
}); });
const turnSubIntoContent = useMemo( const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel);
() => isDefined(subPanel) && isUndefined(contentPanel),
[contentPanel, subPanel]
);
return ( return (
<div <div
@ -227,7 +222,6 @@ export const AppLayout = ({
/> />
)} )}
</div> </div>
<SafariPopup />
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { Fragment, useMemo } from "react"; import React, { Fragment } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { Img } from "components/Img"; import { Img } from "components/Img";
@ -35,11 +35,8 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
const { showLightBox } = useAtomGetter(atoms.lightBox); const { showLightBox } = useAtomGetter(atoms.lightBox);
/* eslint-disable no-irregular-whitespace */ /* eslint-disable no-irregular-whitespace */
const text = useMemo( const text = `${preprocessMarkDawn(rawText, playerName)}
() => `${preprocessMarkDawn(rawText, playerName)} `;
`,
[playerName, rawText]
);
/* eslint-enable no-irregular-whitespace */ /* eslint-enable no-irregular-whitespace */
if (isUndefined(text) || text === "") { if (isUndefined(text) || text === "") {
@ -219,19 +216,17 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
interface TableOfContentsProps { interface TableOfContentsProps {
text: string; text: string;
title?: string; title?: string;
horizontalLine?: boolean; horizontalLine?: boolean;
} }
export const TableOfContents = ({ export const TableOfContents = ({
text, text,
title, title,
horizontalLine = false, horizontalLine = false,
}: TableOfContentsProps): JSX.Element => { }: TableOfContentsProps): JSX.Element => {
const router = useRouter(); const router = useRouter();
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
const toc = useMemo(() => getTocFromMarkdawn(preprocessMarkDawn(text), title), [text, title]); const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
return ( return (
<> <>
@ -268,27 +263,24 @@ interface HeaderProps {
const Header = ({ level, title, slug }: HeaderProps): JSX.Element => { const Header = ({ level, title, slug }: HeaderProps): JSX.Element => {
const isHoverable = useDeviceSupportsHover(); const isHoverable = useDeviceSupportsHover();
const innerComponent = useMemo( const innerComponent = (
() => ( <>
<> <div className="ml-10 flex place-items-center gap-4">
<div className="ml-10 flex place-items-center gap-4"> {title === "* * *" ? (
{title === "* * *" ? ( <div className="mt-8 mb-12 space-x-3 text-dark">
<div className="mt-8 mb-12 space-x-3 text-dark"> <Ico icon={Icon.Emergency} />
<Ico icon={Icon.Emergency} /> <Ico icon={Icon.Emergency} />
<Ico icon={Icon.Emergency} /> <Ico icon={Icon.Emergency} />
<Ico icon={Icon.Emergency} /> </div>
</div> ) : (
) : ( <div className="font-headers">{title}</div>
<div className="font-headers">{title}</div> )}
)} <AnchorShare
<AnchorShare className={cIf(isHoverable, "opacity-0 transition-opacity group-hover:opacity-100")}
className={cIf(isHoverable, "opacity-0 transition-opacity group-hover:opacity-100")} id={slug}
id={slug} />
/> </div>
</div> </>
</>
),
[isHoverable, slug, title]
); );
switch (level) { switch (level) {
@ -349,8 +341,7 @@ const TocLevel = ({
allowIntersection = true, allowIntersection = true,
}: LevelProps): JSX.Element => { }: LevelProps): JSX.Element => {
const router = useRouter(); const router = useRouter();
const ids = tocchildren.map((child) => child.slug);
const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
const currentIntersection = useIntersectionList(ids); const currentIntersection = useIntersectionList(ids);
return ( return (

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MouseEventHandler, useCallback, useMemo } from "react"; import { MouseEventHandler, useCallback } from "react";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { ToolTip } from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
@ -39,10 +39,7 @@ export const NavOption = ({
onClick, onClick,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const router = useRouter(); const router = useRouter();
const isActive = useMemo( const isActive = active || router.asPath.startsWith(url);
() => active || router.asPath.startsWith(url),
[active, router.asPath, url]
);
return ( return (
<ToolTip <ToolTip

View File

@ -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&rsquo;t be a problem but our website isfor some obscure
reasonperforming 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>
);
};

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { ButtonGroup } from "components/Inputs/ButtonGroup";
@ -34,12 +34,8 @@ export const SettingsPopup = (): JSX.Element => {
const router = useRouter(); const router = useRouter();
const currencyOptions = useMemo( const currencyOptions = filterHasAttributes(currencies, ["attributes"] as const).map(
() => (currentCurrency) => currentCurrency.attributes.code
filterHasAttributes(currencies, ["attributes"] as const).map(
(currentCurrency) => currentCurrency.attributes.code
),
[currencies]
); );
const [currencySelect, setCurrencySelect] = useState<number>(-1); const [currencySelect, setCurrencySelect] = useState<number>(-1);

View File

@ -1,4 +1,4 @@
import { Fragment, useCallback, useMemo } from "react"; import { Fragment, useCallback } from "react";
import { AppLayout, AppLayoutRequired } from "./AppLayout"; import { AppLayout, AppLayoutRequired } from "./AppLayout";
import { Chip } from "./Chip"; import { Chip } from "./Chip";
import { HorizontalLine } from "./HorizontalLine"; import { HorizontalLine } from "./HorizontalLine";
@ -59,140 +59,104 @@ export const PostPage = ({
), ),
}); });
const { thumbnail, body, title, excerpt } = useMemo( const thumbnail =
() => ({ selectedTranslation?.thumbnail?.data?.attributes ?? post.thumbnail?.data?.attributes;
thumbnail: const body = selectedTranslation?.body ?? "";
selectedTranslation?.thumbnail?.data?.attributes ?? post.thumbnail?.data?.attributes, const title = selectedTranslation?.title ?? prettySlug(post.slug);
body: selectedTranslation?.body ?? "", const excerpt = selectedTranslation?.excerpt ?? "";
title: selectedTranslation?.title ?? prettySlug(post.slug),
excerpt: selectedTranslation?.excerpt ?? "",
}),
[post.slug, post.thumbnail, selectedTranslation]
);
const subPanel = useMemo( const subPanel =
() => returnHref || returnTitle || displayCredits || displayToc ? (
returnHref || returnTitle || displayCredits || displayToc ? ( <SubPanel>
<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>
{returnHref && returnTitle && ( {returnHref && returnTitle && (
<ReturnButton <ReturnButton href={returnHref} title={returnTitle} displayOnlyOn={"3ColumnsLayout"} />
href={returnHref}
title={returnTitle}
displayOnlyOn={"1ColumnLayout"}
className="mb-10"
/>
)} )}
{displayThumbnailHeader ? ( {displayCredits && (
<> <>
<ThumbnailHeader <HorizontalLine />
thumbnail={thumbnail}
title={title} {selectedTranslation && (
description={excerpt} <div className="grid grid-flow-col place-content-center place-items-center gap-2">
categories={post.categories} <p className="font-headers font-bold">{langui.status}:</p>
languageSwitcher={
languageSwitcherProps.locales.size > 1 ? ( <ToolTip
<LanguageSwitcher {...languageSwitcherProps} /> content={getStatusDescription(selectedTranslation.status, langui)}
) : undefined maxWidth={"20rem"}>
} <Chip text={selectedTranslation.status} />
/> </ToolTip>
</>
) : (
<>
{displayLanguageSwitcher && (
<div className="grid place-content-end place-items-start">
<LanguageSwitcher {...languageSwitcherProps} />
</div> </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} {displayToc && <TableOfContents text={body} title={title} horizontalLine />}
{body && ( </SubPanel>
<> ) : undefined;
{displayThumbnailHeader && <HorizontalLine />}
<Markdawn text={body} />
</>
)}
{appendBody} const contentPanel = (
</ContentPanel> <ContentPanel>
), {returnHref && returnTitle && (
[ <ReturnButton
LanguageSwitcher, href={returnHref}
appendBody, title={returnTitle}
body, displayOnlyOn={"1ColumnLayout"}
displayLanguageSwitcher, className="mb-10"
displayThumbnailHeader, />
displayTitle, )}
excerpt,
languageSwitcherProps, {displayThumbnailHeader ? (
post.categories, <>
prependBody, <ThumbnailHeader
returnHref, thumbnail={thumbnail}
returnTitle, title={title}
thumbnail, description={excerpt}
title, 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} />; return <AppLayout {...otherProps} contentPanel={contentPanel} subPanel={subPanel} />;

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react"; import { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Chip } from "./Chip"; import { Chip } from "./Chip";
import { Ico, Icon } from "./Ico"; import { Ico, Icon } from "./Ico";
@ -75,40 +75,37 @@ export const PreviewCard = ({
const isHoverable = useDeviceSupportsHover(); const isHoverable = useDeviceSupportsHover();
const router = useRouter(); const router = useRouter();
const metadataJSX = useMemo( const metadataJSX = (
() => ( <>
<> {metadata && (metadata.releaseDate || metadata.price) && (
{metadata && (metadata.releaseDate || metadata.price) && ( <div className="flex w-full flex-row flex-wrap gap-x-3">
<div className="flex w-full flex-row flex-wrap gap-x-3"> {metadata.releaseDate && (
{metadata.releaseDate && ( <p className="text-sm">
<p className="text-sm"> <Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" /> {prettyDate(metadata.releaseDate, router.locale)}
{prettyDate(metadata.releaseDate, router.locale)} </p>
</p> )}
)} {metadata.price && (
{metadata.price && ( <p className="justify-self-end text-sm">
<p className="justify-self-end text-sm"> <Ico icon={Icon.ShoppingCart} className="mr-1 translate-y-[.15em] !text-base" />
<Ico icon={Icon.ShoppingCart} className="mr-1 translate-y-[.15em] !text-base" /> {prettyPrice(metadata.price, currencies, currency)}
{prettyPrice(metadata.price, currencies, currency)} </p>
</p> )}
)} {metadata.views && (
{metadata.views && ( <p className="text-sm">
<p className="text-sm"> <Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" /> {prettyShortenNumber(metadata.views)}
{prettyShortenNumber(metadata.views)} </p>
</p> )}
)} {metadata.author && (
{metadata.author && ( <p className="text-sm">
<p className="text-sm"> <Ico icon={Icon.Person} className="mr-1 translate-y-[.15em] !text-base" />
<Ico icon={Icon.Person} className="mr-1 translate-y-[.15em] !text-base" /> {metadata.author}
{metadata.author} </p>
</p> )}
)} </div>
</div> )}
)} </>
</>
),
[currencies, currency, metadata, router.locale]
); );
return ( return (

View File

@ -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 { useHotkeys } from "react-hotkeys-hook";
import naturalCompare from "string-natural-compare"; import naturalCompare from "string-natural-compare";
import { Chip } from "./Chip"; import { Chip } from "./Chip";
@ -87,17 +87,11 @@ export const SmartList = <T,>({
return items; return items;
}, [items, searchingBy, searchingCaseInsensitive, searchingTerm]); }, [items, searchingBy, searchingCaseInsensitive, searchingTerm]);
const filteredItems = useMemo(() => { const filteredItems = searchFilter().filter(filteringFunction);
const filteredBySearch = searchFilter();
return filteredBySearch.filter(filteringFunction);
}, [filteringFunction, searchFilter]);
const sortedItem = useMemo( const sortedItem = filteredItems.sort(sortingFunction);
() => filteredItems.sort(sortingFunction),
[filteredItems, sortingFunction]
);
const groups = useMemo(() => { const groups = (() => {
const memo: Group<T>[] = []; const memo: Group<T>[] = [];
sortedItem.forEach((item) => { sortedItem.forEach((item) => {
@ -116,9 +110,9 @@ export const SmartList = <T,>({
}); });
}); });
return memo.sort(groupSortingFunction); return memo.sort(groupSortingFunction);
}, [groupCountingFunction, groupSortingFunction, groupingFunction, sortedItem]); })();
const pages = useMemo(() => { const pages = (() => {
const memo: Group<T>[][] = []; const memo: Group<T>[][] = [];
let currentPage: Group<T>[] = []; let currentPage: Group<T>[] = [];
let remainingSlots = paginationItemPerPage; let remainingSlots = paginationItemPerPage;
@ -162,7 +156,7 @@ export const SmartList = <T,>({
} }
return memo; return memo;
}, [groups, paginationItemPerPage]); })();
useHotkeys("left", () => setPage((current) => current - 1), { enabled: page > 0 }); useHotkeys("left", () => setPage((current) => current - 1), { enabled: page > 0 });
useHotkeys("right", () => setPage((current) => current + 1), { useHotkeys("right", () => setPage((current) => current + 1), {

View File

@ -84,6 +84,7 @@ export const useSettings = (): void => {
useEffect(() => { useEffect(() => {
if (preferredLanguages.length === 0) { if (preferredLanguages.length === 0) {
if (isDefinedAndNotEmpty(router.locale) && router.locales) { if (isDefinedAndNotEmpty(router.locale) && router.locales) {
console.log(router.locale, getDefaultPreferredLanguages(router.locale, router.locales));
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales)); setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
} }
} else if (router.locale !== preferredLanguages[0]) { } else if (router.locale !== preferredLanguages[0]) {

View File

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

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useIsClient } from "usehooks-ts"; import { useIsClient } from "usehooks-ts";
import { isDefined } from "helpers/others"; import { isDefined } from "helpers/others";
@ -13,7 +13,7 @@ export const useFullscreen = (
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const isClient = useIsClient(); 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 requestFullscreen = useCallback(() => elem?.requestFullscreen(), [elem]);
const exitFullscreen = useCallback( const exitFullscreen = useCallback(

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { throttle } from "throttle-debounce"; import { throttle } from "throttle-debounce";
import { useIsClient } from "usehooks-ts"; import { useIsClient } from "usehooks-ts";
import { useOnScroll } from "./useOnScroll"; import { useOnScroll } from "./useOnScroll";
@ -10,10 +10,7 @@ export const useIntersectionList = (ids: string[]): number => {
const isClient = useIsClient(); const isClient = useIsClient();
const contentPanel = useMemo( const contentPanel = isClient ? document.getElementById(Ids.ContentPanel) : null;
() => (isClient ? document.getElementById(Ids.ContentPanel) : null),
[isClient]
);
const refreshCurrentIntersection = useCallback( const refreshCurrentIntersection = useCallback(
(scroll: number) => { (scroll: number) => {

14
src/hooks/useIsWebkit.ts Normal file
View File

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

View File

@ -1,10 +1,10 @@
import { useMemo, useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useIsClient } from "usehooks-ts"; import { useIsClient } from "usehooks-ts";
import { Ids } from "types/ids"; import { Ids } from "types/ids";
export const useOnScroll = (id: Ids, onScroll: (scroll: number) => void): void => { export const useOnScroll = (id: Ids, onScroll: (scroll: number) => void): void => {
const isClient = useIsClient(); const isClient = useIsClient();
const elem = useMemo(() => (isClient ? document.querySelector(`#${id}`) : null), [id, isClient]); const elem = isClient ? document.querySelector(`#${id}`) : null;
const listener = useCallback(() => { const listener = useCallback(() => {
if (elem?.scrollTop) { if (elem?.scrollTop) {
onScroll(elem.scrollTop); onScroll(elem.scrollTop);

View File

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; import { Dispatch, SetStateAction, useCallback } from "react";
import { useLocalStorage } from "usehooks-ts"; import { useLocalStorage } from "usehooks-ts";
import { ImageQuality } from "helpers/img"; import { ImageQuality } from "helpers/img";
@ -94,13 +94,8 @@ export const useReaderSettings = (): {
setTeint, setTeint,
]); ]);
const filterSettings = useMemo(
() => ({ bookFold, lighting, paperTexture, teint, dropShadow }),
[bookFold, dropShadow, lighting, paperTexture, teint]
);
return { return {
filterSettings, filterSettings: { bookFold, lighting, paperTexture, teint, dropShadow },
isSidePagesEnabled, isSidePagesEnabled,
pageQuality, pageQuality,
toggleBookFold, toggleBookFold,

View File

@ -3,30 +3,34 @@ import { useEffect, useState } from "react";
import { useCounter } from "usehooks-ts"; import { useCounter } from "usehooks-ts";
import { isDefined } from "helpers/others"; import { isDefined } from "helpers/others";
const NUM_RETRIES = 10;
export const useScrollIntoView = (): void => { export const useScrollIntoView = (): void => {
const router = useRouter(); const router = useRouter();
const { count, increment } = useCounter(0); const { count, increment } = useCounter(0);
const [hasReachedElem, setHasReachedElem] = useState(false); const [hasReachedElem, setHasReachedElem] = useState(false);
useEffect(() => { useEffect(() => {
if (!hasReachedElem) { if (count < NUM_RETRIES)
const indexHash = router.asPath.indexOf("#"); if (!hasReachedElem) {
if (indexHash > 0) { const indexHash = router.asPath.indexOf("#");
const hash = router.asPath.slice(indexHash + 1); if (indexHash > 0) {
const element = document.getElementById(hash); const hash = router.asPath.slice(indexHash + 1);
console.log(element); if (hash !== "") {
if (isDefined(element)) { const element = document.getElementById(hash);
console.log(`[useScrollIntoView] ${hash} found`); if (isDefined(element)) {
element.scrollIntoView(); console.log(`[useScrollIntoView] ${hash} found`);
setHasReachedElem(true); element.scrollIntoView();
} else { setHasReachedElem(true);
console.log(`[useScrollIntoView] ${hash} not found`); } else {
setTimeout(() => { console.log(`[useScrollIntoView] ${hash} not found`);
increment(); setTimeout(() => {
}, 100); increment();
}, 200);
}
}
} }
} }
} }, [router.asPath, hasReachedElem, setHasReachedElem, increment, count]);
}, [increment, router.asPath, count, hasReachedElem, setHasReachedElem]);
useEffect(() => setHasReachedElem(false), [router.asPath]); useEffect(() => setHasReachedElem(false), [router.asPath]);
}; };

View File

@ -36,7 +36,7 @@ export const useSmartLanguage = <T>({
setSelectedTranslationIndex(getPreferredLanguage(preferredLanguages, availableLocales)); setSelectedTranslationIndex(getPreferredLanguage(preferredLanguages, availableLocales));
}, [preferredLanguages, availableLocales, router.locale]); }, [preferredLanguages, availableLocales, router.locale]);
const selectedTranslation = useMemo(() => { const selectedTranslation = (() => {
if (isDefined(selectedTranslationIndex)) { if (isDefined(selectedTranslationIndex)) {
const item = items[selectedTranslationIndex]; const item = items[selectedTranslationIndex];
if (isDefined(item)) { if (isDefined(item)) {
@ -44,7 +44,7 @@ export const useSmartLanguage = <T>({
} }
} }
return undefined; return undefined;
}, [items, selectedTranslationIndex, transform]); })();
const languageSwitcherProps = { const languageSwitcherProps = {
languages: languages, languages: languages,

View File

@ -22,12 +22,14 @@ import { LightBoxProvider } from "contexts/LightBoxProvider";
import { SettingsPopup } from "components/Panels/SettingsPopup"; import { SettingsPopup } from "components/Panels/SettingsPopup";
import { useSettings } from "contexts/settings"; import { useSettings } from "contexts/settings";
import { useContainerQueries } from "contexts/containerQueries"; import { useContainerQueries } from "contexts/containerQueries";
import { useWebkitFixes } from "contexts/webkitFixes";
const AccordsLibraryApp = (props: AppProps): JSX.Element => { const AccordsLibraryApp = (props: AppProps): JSX.Element => {
useLocalData(); useLocalData();
useAppLayout(); useAppLayout();
useSettings(); useSettings();
useContainerQueries(); useContainerQueries();
useWebkitFixes();
return ( return (
<> <>

View File

@ -1,5 +1,4 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { NavOption } from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
@ -20,20 +19,18 @@ interface Props extends AppLayoutRequired {}
const Archives = (props: Props): JSX.Element => { const Archives = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader
<PanelHeader icon={Icon.Inventory}
icon={Icon.Inventory} title={langui.archives}
title={langui.archives} description={langui.archives_description}
description={langui.archives_description} />
/> <HorizontalLine />
<HorizontalLine /> <NavOption title={"Videos"} url="/archives/videos/" border />
<NavOption title={"Videos"} url="/archives/videos/" border /> </SubPanel>
</SubPanel>
),
[langui]
); );
return <AppLayout subPanel={subPanel} {...props} />; return <AppLayout subPanel={subPanel} {...props} />;
}; };
export default Archives; export default Archives;

View File

@ -1,5 +1,5 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo, useState } from "react"; import { useState } from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Switch } from "components/Inputs/Switch"; 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 [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton
<ReturnButton href="/archives/videos/"
href="/archives/videos/" title={langui.videos}
title={langui.videos} displayOnlyOn={"3ColumnsLayout"}
displayOnlyOn={"3ColumnsLayout"} className="mb-10"
className="mb-10" />
/>
<PanelHeader <PanelHeader
icon={Icon.Movie} icon={Icon.Movie}
title={langui.videos} title={langui.videos}
description={langui.archives_description} description={langui.archives_description}
/> />
<HorizontalLine /> <HorizontalLine />
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? "Search title..."} placeholder={langui.search_title ?? "Search title..."}
value={searchName} value={searchName}
onChange={setSearchName} onChange={setSearchName}
/> />
{hoverable && ( {hoverable && (
<WithLabel label={langui.always_show_info}> <WithLabel label={langui.always_show_info}>
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} /> <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
</WithLabel> </WithLabel>
)} )}
</SubPanel> </SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList
<SmartList items={filterHasAttributes(channel?.videos?.data, ["id", "attributes"] as const)}
items={filterHasAttributes(channel?.videos?.data, ["id", "attributes"] as const)} getItemId={(item) => item.id}
getItemId={(item) => item.id} renderItem={({ item }) => (
renderItem={({ item }) => ( <PreviewCard
<PreviewCard href={`/archives/videos/v/${item.attributes.uid}`}
href={`/archives/videos/v/${item.attributes.uid}`} title={item.attributes.title}
title={item.attributes.title} thumbnail={getVideoThumbnailURL(item.attributes.uid)}
thumbnail={getVideoThumbnailURL(item.attributes.uid)} thumbnailAspectRatio="16/9"
thumbnailAspectRatio="16/9" thumbnailForceAspectRatio
thumbnailForceAspectRatio keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} metadata={{
metadata={{ releaseDate: item.attributes.published_date,
releaseDate: item.attributes.published_date, views: item.attributes.views,
views: item.attributes.views, author: channel?.title,
author: channel?.title, position: "Top",
position: "Top", }}
}} hoverlay={{
hoverlay={{ __typename: "Video",
__typename: "Video", duration: item.attributes.duration,
duration: item.attributes.duration, }}
}} />
/> )}
)} className={cIf(
className={cIf( isContentPanelAtLeast4xl,
isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-x-3 gap-y-5"
"grid-cols-2 gap-x-3 gap-y-5" )}
)} groupingFunction={() => [channel?.title ?? ""]}
groupingFunction={() => [channel?.title ?? ""]} paginationItemPerPage={25}
paginationItemPerPage={25} searchingTerm={searchName}
searchingTerm={searchName} searchingBy={(item) => item.attributes.title}
searchingBy={(item) => item.attributes.title} />
/> </ContentPanel>
</ContentPanel>
),
[channel?.title, channel?.videos?.data, isContentPanelAtLeast4xl, keepInfoVisible, searchName]
); );
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />; return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useMemo, useState } from "react"; import { useState } from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { SmartList } from "components/SmartList"; 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 [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton
<ReturnButton href="/archives/"
href="/archives/" title={"Archives"}
title={"Archives"} displayOnlyOn={"3ColumnsLayout"}
displayOnlyOn={"3ColumnsLayout"} className="mb-10"
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 <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? "Search title..."} placeholder={langui.search_title ?? "Search title..."}
value={searchName} value={searchName}
onChange={setSearchName} onChange={setSearchName}
/> />
{hoverable && ( {hoverable && (
<WithLabel label={langui.always_show_info}> <WithLabel label={langui.always_show_info}>
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} /> <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
</WithLabel> </WithLabel>
)} )}
</SubPanel> </SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList
<SmartList items={filterHasAttributes(videos, ["id", "attributes"] as const)}
items={filterHasAttributes(videos, ["id", "attributes"] as const)} getItemId={(item) => item.id}
getItemId={(item) => item.id} renderItem={({ item }) => (
renderItem={({ item }) => ( <PreviewCard
<PreviewCard href={`/archives/videos/v/${item.attributes.uid}`}
href={`/archives/videos/v/${item.attributes.uid}`} title={item.attributes.title}
title={item.attributes.title} thumbnail={getVideoThumbnailURL(item.attributes.uid)}
thumbnail={getVideoThumbnailURL(item.attributes.uid)} thumbnailAspectRatio="16/9"
thumbnailAspectRatio="16/9" thumbnailForceAspectRatio
thumbnailForceAspectRatio keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} metadata={{
metadata={{ releaseDate: item.attributes.published_date,
releaseDate: item.attributes.published_date, views: item.attributes.views,
views: item.attributes.views, author: item.attributes.channel?.data?.attributes?.title,
author: item.attributes.channel?.data?.attributes?.title, position: "Top",
position: "Top", }}
}} hoverlay={{
hoverlay={{ __typename: "Video",
__typename: "Video", duration: item.attributes.duration,
duration: item.attributes.duration, }}
}} />
/> )}
)} className={cIf(
className={cIf( isContentPanelAtLeast4xl,
isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-x-3 gap-y-5"
"grid-cols-2 gap-x-3 gap-y-5" )}
)} paginationItemPerPage={25}
paginationItemPerPage={25} searchingTerm={searchName}
searchingTerm={searchName} searchingBy={(item) => item.attributes.title}
searchingBy={(item) => item.attributes.title} />
/> </ContentPanel>
</ContentPanel>
),
[isContentPanelAtLeast4xl, keepInfoVisible, searchName, videos]
); );
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />; return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
}; };

View File

@ -1,5 +1,4 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
@ -34,120 +33,101 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
const router = useRouter(); const router = useRouter();
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton
<ReturnButton href="/archives/videos/"
href="/archives/videos/" title={langui.videos}
title={langui.videos} displayOnlyOn={"3ColumnsLayout"}
displayOnlyOn={"3ColumnsLayout"} />
/>
<HorizontalLine /> <HorizontalLine />
<NavOption title={langui.video} url="#video" border /> <NavOption title={langui.video} url="#video" border />
<NavOption title={langui.channel} url="#channel" border /> <NavOption title={langui.channel} url="#channel" border />
<NavOption title={langui.description} url="#description" border /> <NavOption title={langui.description} url="#description" border />
</SubPanel> </SubPanel>
),
[langui]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <ReturnButton
<ReturnButton href="/library/"
href="/library/" title={langui.library}
title={langui.library} displayOnlyOn={"1ColumnLayout"}
displayOnlyOn={"1ColumnLayout"} className="mb-10"
className="mb-10" />
/>
<div className="grid place-items-center gap-12"> <div className="grid place-items-center gap-12">
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80"> <div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
{video.gone ? ( {video.gone ? (
<video className="w-full" src={getVideoFile(video.uid)} controls /> <video className="w-full" src={getVideoFile(video.uid)} controls />
) : ( ) : (
<iframe <iframe
src={`https://www.youtube-nocookie.com/embed/${video.uid}`} src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
className="aspect-video w-full" className="aspect-video w-full"
title="YouTube video player" title="YouTube video player"
frameBorder="0" frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; allow="accelerometer; autoplay; clipboard-write;
encrypted-media; gyroscope; picture-in-picture" encrypted-media; gyroscope; picture-in-picture"
allowFullScreen 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>
)} )}
<InsetBox id="description" className="grid place-items-center"> <div className="mt-2 p-6">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> <h1 className="text-2xl">{video.title}</h1>
<h2 className="text-2xl">{langui.description}</h2> <div className="flex w-full flex-row flex-wrap gap-x-6">
<p className="whitespace-pre-line">{video.description}</p> <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> </div>
</InsetBox> </InsetBox>
</div> )}
</ContentPanel>
), <InsetBox id="description" className="grid place-items-center">
[ <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
isContentPanelAtLeast4xl, <h2 className="text-2xl">{langui.description}</h2>
langui, <p className="whitespace-pre-line">{video.description}</p>
router.locale, </div>
video.channel?.data?.attributes, </InsetBox>
video.description, </div>
video.gone, </ContentPanel>
video.likes,
video.published_date,
video.source,
video.title,
video.uid,
video.views,
]
); );
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />; return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;

View File

@ -67,105 +67,80 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element =
), ),
}); });
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel>
<ContentPanel> <ReturnButton
<ReturnButton displayOnlyOn={"1ColumnLayout"}
displayOnlyOn={"1ColumnLayout"} href="/chronicles"
href="/chronicles" title={langui.chronicles}
title={langui.chronicles} className="mb-10"
className="mb-10" />
/>
{isDefined(selectedTranslation) ? ( {isDefined(selectedTranslation) ? (
<> <>
<h1 className="mb-16 text-center text-3xl">{selectedTranslation.title}</h1> <h1 className="mb-16 text-center text-3xl">{selectedTranslation.title}</h1>
{languageSwitcherProps.locales.size > 1 && ( {languageSwitcherProps.locales.size > 1 && (
<LanguageSwitcher {...languageSwitcherProps} /> <LanguageSwitcher {...languageSwitcherProps} />
)} )}
{isDefined(selectedTranslation.body) && ( {isDefined(selectedTranslation.body) && <Markdawn text={selectedTranslation.body.body} />}
<Markdawn text={selectedTranslation.body.body} /> </>
)} ) : (
</> <>
) : ( {selectedContentTranslation && (
<> <>
{selectedContentTranslation && ( <ThumbnailHeader
<> pre_title={selectedContentTranslation.pre_title}
<ThumbnailHeader title={selectedContentTranslation.title}
pre_title={selectedContentTranslation.pre_title} subtitle={selectedContentTranslation.subtitle}
title={selectedContentTranslation.title} languageSwitcher={
subtitle={selectedContentTranslation.subtitle} ContentLanguageSwitcherProps.locales.size > 1 ? (
languageSwitcher={ <ContentLanguageSwitcher {...ContentLanguageSwitcherProps} />
ContentLanguageSwitcherProps.locales.size > 1 ? ( ) : undefined
<ContentLanguageSwitcher {...ContentLanguageSwitcherProps} /> }
) : undefined categories={primaryContent?.categories}
} type={primaryContent?.type}
categories={primaryContent?.categories} description={selectedContentTranslation.description}
type={primaryContent?.type} thumbnail={primaryContent?.thumbnail?.data?.attributes}
description={selectedContentTranslation.description} />
thumbnail={primaryContent?.thumbnail?.data?.attributes}
/>
{selectedContentTranslation.text_set?.text && ( {selectedContentTranslation.text_set?.text && (
<> <>
<HorizontalLine /> <HorizontalLine />
<Markdawn text={selectedContentTranslation.text_set.text} /> <Markdawn text={selectedContentTranslation.text_set.text} />
</> </>
)} )}
</> </>
)} )}
</> </>
)} )}
</ContentPanel> </ContentPanel>
),
[
selectedTranslation,
languageSwitcherProps,
LanguageSwitcher,
selectedContentTranslation,
ContentLanguageSwitcherProps,
ContentLanguageSwitcher,
primaryContent?.categories,
primaryContent?.type,
primaryContent?.thumbnail?.data?.attributes,
langui,
]
); );
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton displayOnlyOn={"3ColumnsLayout"} href="/chronicles" title={langui.chronicles} />
<ReturnButton
displayOnlyOn={"3ColumnsLayout"}
href="/chronicles"
title={langui.chronicles}
/>
<HorizontalLine /> <HorizontalLine />
<div className="grid gap-16"> <div className="grid gap-16">
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map( {filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map((chapter) => (
(chapter) => ( <TranslatedChroniclesList
<TranslatedChroniclesList key={chapter.id}
key={chapter.id} chronicles={chapter.attributes.chronicles.data}
chronicles={chapter.attributes.chronicles.data} translations={filterHasAttributes(chapter.attributes.titles, [
translations={filterHasAttributes(chapter.attributes.titles, [ "language.data.attributes.code",
"language.data.attributes.code", ] as const).map((translation) => ({
] as const).map((translation) => ({ title: translation.title,
title: translation.title, language: translation.language.data.attributes.code,
language: translation.language.data.attributes.code, }))}
}))} fallback={{ title: prettySlug(chapter.attributes.slug) }}
fallback={{ title: prettySlug(chapter.attributes.slug) }} currentSlug={chronicle.slug}
currentSlug={chronicle.slug} />
/> ))}
) </div>
)} </SubPanel>
</div>
</SubPanel>
),
[chapters, chronicle.slug, langui]
); );
return ( return (

View File

@ -1,5 +1,4 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel"; import { SubPanel } from "components/Containers/SubPanel";
@ -26,37 +25,32 @@ interface Props extends AppLayoutRequired {
const Chronicles = ({ chapters, ...otherProps }: Props): JSX.Element => { const Chronicles = ({ chapters, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader
<PanelHeader icon={Icon.WatchLater}
icon={Icon.WatchLater} title={langui.chronicles}
title={langui.chronicles} description={langui.chronicles_description}
description={langui.chronicles_description} />
/>
<HorizontalLine /> <HorizontalLine />
<div className="grid gap-16"> <div className="grid gap-16">
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map( {filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map((chapter) => (
(chapter) => ( <TranslatedChroniclesList
<TranslatedChroniclesList key={chapter.id}
key={chapter.id} chronicles={chapter.attributes.chronicles.data}
chronicles={chapter.attributes.chronicles.data} translations={filterHasAttributes(chapter.attributes.titles, [
translations={filterHasAttributes(chapter.attributes.titles, [ "language.data.attributes.code",
"language.data.attributes.code", ] as const).map((translation) => ({
] as const).map((translation) => ({ title: translation.title,
title: translation.title, language: translation.language.data.attributes.code,
language: translation.language.data.attributes.code, }))}
}))} fallback={{ title: prettySlug(chapter.attributes.slug) }}
fallback={{ title: prettySlug(chapter.attributes.slug) }} />
/> ))}
) </div>
)} </SubPanel>
</div>
</SubPanel>
),
[chapters, langui]
); );
return <AppLayout subPanel={subPanel} {...otherProps} />; return <AppLayout subPanel={subPanel} {...otherProps} />;

View File

@ -1,5 +1,5 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { Fragment, useCallback, useMemo } from "react"; import { Fragment, useCallback } from "react";
import naturalCompare from "string-natural-compare"; import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip"; import { Chip } from "components/Chip";
@ -62,344 +62,305 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]); useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
const { previousContent, nextContent } = useMemo( const previousContent =
() => ({ content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
previousContent: ? getPreviousContent(content.folder.data.attributes.contents.data, content.slug)
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence : undefined;
? getPreviousContent(content.folder.data.attributes.contents.data, content.slug) const nextContent =
: undefined, content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
nextContent: ? getNextContent(content.folder.data.attributes.contents.data, content.slug)
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence : undefined;
? getNextContent(content.folder.data.attributes.contents.data, content.slug)
: undefined,
}),
[content.folder, content.slug]
);
const returnButtonProps = useMemo( const returnButtonProps = {
() => ({ href: content.folder?.data?.attributes
href: content.folder?.data?.attributes ? `/contents/folder/${content.folder.data.attributes.slug}`
? `/contents/folder/${content.folder.data.attributes.slug}` : "/contents",
: "/contents",
translations: filterHasAttributes(content.folder?.data?.attributes?.titles, [ translations: filterHasAttributes(content.folder?.data?.attributes?.titles, [
"language.data.attributes.code", "language.data.attributes.code",
] as const).map((title) => ({ ] as const).map((title) => ({
language: title.language.data.attributes.code, language: title.language.data.attributes.code,
title: title.title, title: title.title,
})), })),
fallback: { fallback: {
title: content.folder?.data?.attributes title: content.folder?.data?.attributes
? prettySlug(content.folder.data.attributes.slug) ? prettySlug(content.folder.data.attributes.slug)
: langui.contents, : langui.contents,
}, },
langui, langui,
}), };
[content.folder?.data?.attributes, langui]
);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" />
<TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" />
{selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && ( {selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
<> <>
<HorizontalLine /> <HorizontalLine />
<div className="grid gap-5"> <div className="grid gap-5">
<h2 className="text-xl"> <h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes.code === {selectedTranslation.text_set.source_language.data.attributes.code ===
selectedTranslation.language?.data?.attributes?.code selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice ? langui.transcript_notice
: langui.translation_notice} : langui.translation_notice}
</h2> </h2>
{selectedTranslation.text_set.source_language.data.attributes.code !== {selectedTranslation.text_set.source_language.data.attributes.code !==
selectedTranslation.language?.data?.attributes?.code && ( selectedTranslation.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2"> <div className="grid place-items-center gap-2">
<p className="font-headers font-bold">{langui.source_language}:</p> <p className="font-headers font-bold">{langui.source_language}:</p>
<Chip <Chip
text={prettyLanguage( text={prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes.code, selectedTranslation.text_set.source_language.data.attributes.code,
languages 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>
</div> </div>
)}
{selectedTranslation.text_set.transcribers && <div className="grid grid-flow-col place-content-center place-items-center gap-2">
selectedTranslation.text_set.transcribers.data.length > 0 && ( <p className="font-headers font-bold">{langui.status}:</p>
<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>
)}
{selectedTranslation.text_set.translators && <ToolTip
selectedTranslation.text_set.translators.data.length > 0 && ( content={getStatusDescription(selectedTranslation.text_set.status, langui)}
<div> maxWidth={"20rem"}>
<p className="font-headers font-bold">{langui.translators}:</p> <Chip text={selectedTranslation.text_set.status} />
<div className="grid place-content-center place-items-center gap-2"> </ToolTip>
{filterHasAttributes(selectedTranslation.text_set.translators.data, [ </div>
"attributes",
"id",
] as const).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip recorder={recorder.attributes} />
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.proofreaders && {selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.proofreaders.data.length > 0 && ( selectedTranslation.text_set.transcribers.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> <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"> <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>
</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>
</> </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 && ( {selectedTranslation?.text_set?.text && (
<> <>
<TableOfContents <HorizontalLine />
text={selectedTranslation.text_set.text} <Markdawn text={selectedTranslation.text_set.text} />
title={prettyInlineTitle( </>
selectedTranslation.pre_title, )}
selectedTranslation.title,
selectedTranslation.subtitle {nextContent?.attributes && (
)} <>
horizontalLine <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>
{content.ranged_contents?.data && content.ranged_contents.data.length > 0 && ( </ContentPanel>
<>
<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,
]
); );
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />; return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react"; import { useState, useCallback } from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import naturalCompare from "string-natural-compare"; import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
@ -116,163 +116,134 @@ const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
[searchName] [searchName]
); );
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader
<PanelHeader icon={Icon.Workspaces}
icon={Icon.Workspaces} title={langui.contents}
title={langui.contents} description={langui.contents_description}
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 <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."} placeholder={langui.search_title ?? "Search..."}
value={searchName} value={searchName}
onChange={(name) => { onChange={(name) => {
setSearchName(name); setSearchName(name);
if (isDefinedAndNotEmpty(name)) { if (isDefinedAndNotEmpty(name)) {
sendAnalytics("Contents/All", "Change search term"); sendAnalytics("Contents/All", "Change search term");
} else { } else {
sendAnalytics("Contents/All", "Clear search term"); 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}> {hoverable && (
<Select <WithLabel label={langui.always_show_info}>
className="w-full" <Switch
options={[langui.category ?? "Category", langui.type ?? "Type"]} value={keepInfoVisible}
value={groupingMethod} onClick={() => {
onChange={(value) => { toggleKeepInfoVisible();
setGroupingMethod(value); sendAnalytics("Contents/All", `Always ${keepInfoVisible ? "hide" : "show"} info`);
sendAnalytics(
"Contents/All",
`Change grouping method (${["none", "category", "type"][value + 1]})`
);
}} }}
allowEmpty
/> />
</WithLabel> </WithLabel>
)}
{hoverable && ( <Button
<WithLabel label={langui.always_show_info}> className="mt-8"
<Switch text={langui.reset_all_filters}
value={keepInfoVisible} icon={Icon.Replay}
onClick={() => { onClick={() => {
toggleKeepInfoVisible(); setSearchName(DEFAULT_FILTERS_STATE.searchName);
sendAnalytics("Contents/All", `Always ${keepInfoVisible ? "hide" : "show"} info`); setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
}} setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
/> sendAnalytics("Contents/All", "Reset all filters");
</WithLabel> }}
)} />
</SubPanel>
<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,
]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList
<SmartList items={filterHasAttributes(contents, ["attributes", "id"] as const)}
items={filterHasAttributes(contents, ["attributes", "id"] as const)} getItemId={(item) => item.id}
getItemId={(item) => item.id} renderItem={({ item }) => (
renderItem={({ item }) => ( <TranslatedPreviewCard
<TranslatedPreviewCard href={`/contents/${item.attributes.slug}`}
href={`/contents/${item.attributes.slug}`} translations={filterHasAttributes(item.attributes.translations, [
translations={filterHasAttributes(item.attributes.translations, [ "language.data.attributes.code",
"language.data.attributes.code", ] as const).map((translation) => ({
] as const).map((translation) => ({ pre_title: translation.pre_title,
pre_title: translation.pre_title, title: translation.title,
title: translation.title, subtitle: translation.subtitle,
subtitle: translation.subtitle, language: translation.language.data.attributes.code,
language: translation.language.data.attributes.code, }))}
}))} fallback={{ title: prettySlug(item.attributes.slug) }}
fallback={{ title: prettySlug(item.attributes.slug) }} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="3/2"
thumbnailAspectRatio="3/2" thumbnailForceAspectRatio
thumbnailForceAspectRatio topChips={
topChips={ item.attributes.type?.data?.attributes
item.attributes.type?.data?.attributes ? [
? [ item.attributes.type.data.attributes.titles?.[0]
item.attributes.type.data.attributes.titles?.[0] ? item.attributes.type.data.attributes.titles[0]?.title
? item.attributes.type.data.attributes.titles[0]?.title : prettySlug(item.attributes.type.data.attributes.slug),
: prettySlug(item.attributes.type.data.attributes.slug), ]
] : undefined
: undefined }
} bottomChips={item.attributes.categories?.data.map(
bottomChips={item.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} />
/> )}
)} className={cJoin(
className={cJoin( "items-end",
"items-end", cIf(
cIf( isContentPanelAtLeast4xl,
isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-x-3 gap-y-5"
"grid-cols-2 gap-x-3 gap-y-5" )
) )}
)} groupingFunction={groupingFunction}
groupingFunction={groupingFunction} filteringFunction={filteringFunction}
filteringFunction={filteringFunction} searchingTerm={searchName}
searchingTerm={searchName} searchingBy={(item) =>
searchingBy={(item) => `
`
${item.attributes.slug} ${item.attributes.slug}
${filterDefined(item.attributes.translations) ${filterDefined(item.attributes.translations)
.map((translation) => .map((translation) =>
prettyInlineTitle(translation.pre_title, translation.title, translation.subtitle) prettyInlineTitle(translation.pre_title, translation.title, translation.subtitle)
) )
.join(" ")}` .join(" ")}`
} }
paginationItemPerPage={50} paginationItemPerPage={50}
/> />
</ContentPanel> </ContentPanel>
),
[
isContentPanelAtLeast4xl,
contents,
filteringFunction,
groupingFunction,
keepInfoVisible,
searchName,
]
); );
return ( return (

View File

@ -1,5 +1,4 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo } from "react";
import naturalCompare from "string-natural-compare"; import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel"; 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 langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl); const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader
<PanelHeader icon={Icon.Workspaces}
icon={Icon.Workspaces} title={langui.contents}
title={langui.contents} description={langui.contents_description}
description={langui.contents_description} />
/>
<HorizontalLine /> <HorizontalLine />
<Button href="/contents/all" text={langui.switch_to_grid_view} icon={Icon.Apps} /> <Button href="/contents/all" text={langui.switch_to_grid_view} icon={Icon.Apps} />
</SubPanel> </SubPanel>
),
[langui.contents, langui.contents_description, langui.switch_to_grid_view]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
<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 && ( <>
<> {folder.parent_folder.data.attributes.slug === "root" ? (
{folder.parent_folder.data.attributes.slug === "root" ? ( <Button href="/contents" icon={Icon.Home} />
<Button href="/contents" icon={Icon.Home} /> ) : (
) : ( <TranslatedButton
<TranslatedButton href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`} translations={filterHasAttributes(folder.parent_folder.data.attributes.titles, [
translations={filterHasAttributes(folder.parent_folder.data.attributes.titles, [ "language.data.attributes.code",
"language.data.attributes.code", ] as const).map((title) => ({
] as const).map((title) => ({ language: title.language.data.attributes.code,
language: title.language.data.attributes.code, text: title.title,
text: title.title, }))}
}))} fallback={{
fallback={{ text: prettySlug(folder.parent_folder.data.attributes.slug),
text: prettySlug(folder.parent_folder.data.attributes.slug), }}
}} />
/> )}
)} <Ico icon={Icon.ChevronRight} />
<Ico icon={Icon.ChevronRight} /> </>
</> )}
)}
{folder.slug === "root" ? ( {folder.slug === "root" ? (
<Button href="/contents" icon={Icon.Home} active /> <Button href="/contents" icon={Icon.Home} active />
) : ( ) : (
<TranslatedButton <TranslatedButton
translations={filterHasAttributes(folder.titles, [ translations={filterHasAttributes(folder.titles, [
"language.data.attributes.code", "language.data.attributes.code",
] as const).map((title) => ({ ] as const).map((title) => ({
language: title.language.data.attributes.code, language: title.language.data.attributes.code,
text: title.title, text: title.title,
}))} }))}
fallback={{ fallback={{
text: prettySlug(folder.slug), text: prettySlug(folder.slug),
}} }}
active active
/> />
)} )}
</div> </div>
<SmartList <SmartList
items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)} items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)}
getItemId={(item) => item.id} getItemId={(item) => item.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<TranslatedPreviewFolder <TranslatedPreviewFolder
href={`/contents/folder/${item.attributes.slug}`} href={`/contents/folder/${item.attributes.slug}`}
translations={filterHasAttributes(item.attributes.titles, [ translations={filterHasAttributes(item.attributes.titles, [
"language.data.attributes.code", "language.data.attributes.code",
] as const).map((title) => ({ ] as const).map((title) => ({
title: title.title, title: title.title,
language: title.language.data.attributes.code, language: title.language.data.attributes.code,
}))} }))}
fallback={{ title: prettySlug(item.attributes.slug) }} fallback={{ title: prettySlug(item.attributes.slug) }}
/> />
)} )}
className={cJoin( className={cJoin(
"items-end", "items-end",
cIf( 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(
isContentPanelAtLeast4xl, isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-3 gap-y-5" "grid-cols-2 gap-4"
)} )
renderWhenEmpty={() => <></>}
groupingFunction={() => [langui.contents ?? "Contents"]}
/>
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
<NoContentNorFolderMessage />
)} )}
</ContentPanel> renderWhenEmpty={() => <></>}
), groupingFunction={() => [langui.folders ?? "Folders"]}
[ />
folder.contents?.data,
folder.parent_folder?.data?.attributes, <SmartList
folder.slug, items={filterHasAttributes(folder.contents?.data, ["id", "attributes"] as const)}
folder.subfolders?.data, getItemId={(item) => item.id}
folder.titles, renderItem={({ item }) => (
isContentPanelAtLeast4xl, <TranslatedPreviewCard
langui, 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 ( return (

View File

@ -1,5 +1,4 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip"; import { Chip } from "components/Chip";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
@ -23,56 +22,53 @@ interface Props extends AppLayoutRequired {
} }
const CheckupContents = ({ contents, ...otherProps }: Props): JSX.Element => { const CheckupContents = ({ contents, ...otherProps }: Props): JSX.Element => {
const testReport = useMemo(() => testingContent(contents), [contents]); const testReport = testingContent(contents);
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> {<h2 className="text-2xl">{testReport.title}</h2>}
{<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"> <div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p /> <p />
<p /> <p />
<p className="font-headers">Ref</p> <p className="font-headers">Ref</p>
<p className="font-headers">Name</p> <p className="font-headers">Name</p>
<p className="font-headers">Type</p> <p className="font-headers">Type</p>
<p className="font-headers">Severity</p> <p className="font-headers">Severity</p>
<p className="font-headers">Description</p> <p className="font-headers">Description</p>
</div> </div>
{testReport.lines {testReport.lines
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => b.severity - a.severity) .sort((a, b) => b.severity - a.severity)
.map((line, index) => ( .map((line, index) => (
<div <div
key={index} key={index}
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
justify-items-start gap-2"> justify-items-start gap-2">
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab /> <Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab /> <Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
<p>{line.subitems.join(" -> ")}</p> <p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p> <p>{line.name}</p>
<Chip text={line.type} /> <Chip text={line.type} />
<Chip <Chip
className={ className={
line.severity === Severity.VeryHigh line.severity === Severity.VeryHigh
? "bg-[#f00] font-bold !opacity-100" ? "bg-[#f00] font-bold !opacity-100"
: line.severity === Severity.High : line.severity === Severity.High
? "bg-[#ff6600] font-bold !opacity-100" ? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === Severity.Medium : line.severity === Severity.Medium
? "bg-[#fff344] !opacity-100" ? "bg-[#fff344] !opacity-100"
: "" : ""
} }
text={Severity[line.severity]} text={Severity[line.severity]}
/> />
<ToolTip content={line.recommandation} placement="left"> <ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p> <p>{line.description}</p>
</ToolTip> </ToolTip>
</div> </div>
))} ))}
</ContentPanel> </ContentPanel>
),
[testReport.lines, testReport.title]
); );
return <AppLayout contentPanel={contentPanel} {...otherProps} />; return <AppLayout contentPanel={contentPanel} {...otherProps} />;

View File

@ -1,5 +1,4 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip"; import { Chip } from "components/Chip";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
@ -27,54 +26,51 @@ interface Props extends AppLayoutRequired {
const CheckupLibraryItems = ({ libraryItems, ...otherProps }: Props): JSX.Element => { const CheckupLibraryItems = ({ libraryItems, ...otherProps }: Props): JSX.Element => {
const testReport = testingLibraryItem(libraryItems); const testReport = testingLibraryItem(libraryItems);
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> {<h2 className="text-2xl">{testReport.title}</h2>}
{<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"> <div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p /> <p />
<p /> <p />
<p className="font-headers">Ref</p> <p className="font-headers">Ref</p>
<p className="font-headers">Name</p> <p className="font-headers">Name</p>
<p className="font-headers">Type</p> <p className="font-headers">Type</p>
<p className="font-headers">Severity</p> <p className="font-headers">Severity</p>
<p className="font-headers">Description</p> <p className="font-headers">Description</p>
</div> </div>
{testReport.lines {testReport.lines
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => b.severity - a.severity) .sort((a, b) => b.severity - a.severity)
.map((line, index) => ( .map((line, index) => (
<div <div
key={index} key={index}
className="mb-2 grid className="mb-2 grid
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2"> 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.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab /> <Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
<p>{line.subitems.join(" -> ")}</p> <p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p> <p>{line.name}</p>
<Chip text={line.type} /> <Chip text={line.type} />
<Chip <Chip
className={ className={
line.severity === Severity.VeryHigh line.severity === Severity.VeryHigh
? "bg-[#f00] font-bold !opacity-100" ? "bg-[#f00] font-bold !opacity-100"
: line.severity === Severity.High : line.severity === Severity.High
? "bg-[#ff6600] font-bold !opacity-100" ? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === Severity.Medium : line.severity === Severity.Medium
? "bg-[#fff344] !opacity-100" ? "bg-[#fff344] !opacity-100"
: "" : ""
} }
text={Severity[line.severity]} text={Severity[line.severity]}
/> />
<ToolTip content={line.recommandation} placement="left"> <ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p> <p>{line.description}</p>
</ToolTip> </ToolTip>
</div> </div>
))} ))}
</ContentPanel> </ContentPanel>
),
[testReport.lines, testReport.title]
); );
return <AppLayout contentPanel={contentPanel} {...otherProps} />; return <AppLayout contentPanel={contentPanel} {...otherProps} />;

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import TurndownService from "turndown"; import TurndownService from "turndown";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
@ -156,250 +156,243 @@ const Editor = (props: Props): JSX.Element => {
[transformationWrapper] [transformationWrapper]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <Popup isVisible={converterOpened} onCloseRequest={() => setConverterOpened(false)}>
<Popup isVisible={converterOpened} onCloseRequest={() => setConverterOpened(false)}> <div className="text-center">
<div className="text-center"> <h2 className="mt-4">Convert HTML to markdown</h2>
<h2 className="mt-4">Convert HTML to markdown</h2> <p>
<p> Copy and paste any HTML content (content from web pages) here.
Copy and paste any HTML content (content from web pages) here. <br />
<br /> The text will immediatly be converted to valid Markdown.
The text will immediatly be converted to valid Markdown. <br />
<br /> You can then copy the converted text and paste it anywhere you want in the editor
You can then copy the converted text and paste it anywhere you want in the editor </p>
</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 &ldquo;x&rdquo;</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&rsquo;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&rsquo;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>
<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"> let paste = event.clipboardData.getData("text/html");
<div> paste = paste.replace(/<!--.*?-->/u, "");
<h2>Editor</h2> paste = turndownService.turndown(paste);
<textarea paste = paste.replace(/<!--.*?-->/u, "");
ref={textAreaRef}
onInput={(event) => { const target = event.target as HTMLTextAreaElement;
const textarea = event.target as HTMLTextAreaElement; target.value = paste;
handleInput(textarea.value); target.select();
}} event.preventDefault();
className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8 }}
font-mono text-black outline-none" className="h-[50vh] w-[50vw]"
value={markdown} />
title="Input textarea" </Popup>
/>
</div> <div className="mb-4 flex flex-row gap-2">
<div> <ToolTip
<h2>Preview</h2> content={
<div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8"> <div className="grid gap-2">
<Markdawn className="w-full" text={markdown} /> <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> </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 &ldquo;x&rdquo;</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&rsquo;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&rsquo;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>
</div>
<div className="mt-8"> <div className="mt-8">
<TableOfContents text={markdown} /> <TableOfContents text={markdown} />
</div> </div>
</ContentPanel> </ContentPanel>
),
[appendDoc, converterOpened, handleInput, insert, markdown, preline, toggleWrap, wrap]
); );
return <AppLayout contentPanel={contentPanel} {...props} />; return <AppLayout contentPanel={contentPanel} {...props} />;

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { ButtonGroup } from "components/Inputs/ButtonGroup";
@ -359,183 +359,168 @@ const Transcript = (props: Props): JSX.Element => {
[updateDisplayedText] [updateDisplayedText]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full} className="overflow-hidden !pr-0 !pt-4">
<ContentPanel width={ContentPanelWidthSizes.Full} className="overflow-hidden !pr-0 !pt-4"> <div className="grid grid-flow-col grid-cols-[1fr_5rem]">
<div className="grid grid-flow-col grid-cols-[1fr_5rem]"> <textarea
<textarea ref={textAreaRef}
ref={textAreaRef} onChange={updateDisplayedText}
onChange={updateDisplayedText} onClick={updateLineIndex}
onClick={updateLineIndex} onKeyUp={updateLineIndex}
onKeyUp={updateLineIndex} title="Input textarea"
title="Input textarea" className="whitespace-pre"
className="whitespace-pre" />
/>
<p <p
className="h-[80vh] whitespace-nowrap font-[initial] font-bold className="h-[80vh] whitespace-nowrap font-[initial] font-bold
[writing-mode:vertical-rl] [transform-origin:top_right]" [writing-mode:vertical-rl] [transform-origin:top_right]"
style={{ style={{
transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`, transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`,
}}> }}>
{text.split("\n")[lineIndex]} {text.split("\n")[lineIndex]}
</p> </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>
<div className="flex flex-wrap place-items-center gap-4 pr-24"> <div className="grid place-items-center">
<div className="grid place-items-center"> <p>Font size: {fontSize}x</p>
<p>Text offset: {xOffset}px</p> <input
<input title="Font size multiplier"
title="Font size multiplier" type="range"
type="range" min="1000"
min="0" max="3000"
max="100" value={fontSize * SIZE_MULTIPLIER}
value={xOffset * 10} onChange={(event) => setFontSize(parseInt(event.target.value, 10) / SIZE_MULTIPLIER)}
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 ⟹ " 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> </div>
</ContentPanel> <ToolTip content="Automatically convert Western punctuations to Japanese ones.">
), <Button text=". ⟹ 。" onClick={convertPunctuation} />
[ </ToolTip>
convertFullWidth, <ToolTip content="Swap a kana for one of its variant (different diacritics).">
convertPunctuation, <Button text="か ⟺ が" onClick={toggleDakuten} />
fontSize, </ToolTip>
insert, <ToolTip content="Toggle a kana's small form">
lineIndex, <Button text="つ ⟺ っ" onClick={toggleSmallForm} />
text, </ToolTip>
toggleDakuten, <ToolTip content="Convert standard characters to their full width variant.">
toggleSmallForm, <Button text="123 ⟹ " onClick={convertFullWidth} />
updateDisplayedText, </ToolTip>
updateLineIndex,
xOffset, <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} />; return <AppLayout contentPanel={contentPanel} {...props} contentPanelScroolbar={false} />;

View File

@ -1,4 +1,4 @@
import { Fragment, useCallback, useMemo } from "react"; import { Fragment, useCallback } from "react";
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
@ -88,511 +88,467 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
useScrollTopOnChange(Ids.ContentPanel, [item]); useScrollTopOnChange(Ids.ContentPanel, [item]);
const currentIntersection = useIntersectionList(intersectionIds); const currentIntersection = useIntersectionList(intersectionIds);
const isVariantSet = useMemo( const isVariantSet =
() => item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" && item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set",
[item.metadata] const displayOpenScans = item.contents?.data.some(
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
); );
const displayOpenScans = useMemo( const subPanel = (
() => <SubPanel>
item.contents?.data.some( <ReturnButton href="/library/" title={langui.library} displayOnlyOn="3ColumnsLayout" />
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
), <HorizontalLine />
[item.contents?.data]
<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( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<SubPanel> <ReturnButton
<ReturnButton href="/library/" title={langui.library} displayOnlyOn="3ColumnsLayout" /> href="/library/"
title={langui.library}
<HorizontalLine /> displayOnlyOn="1ColumnLayout"
className="mb-10"
<div className="grid gap-4"> />
<NavOption <div className="grid place-items-center gap-12">
title={langui.summary} <div
url={`#${intersectionIds[0]}`} className={cJoin(
border "relative h-[50vh] w-full cursor-pointer drop-shadow-xl shadow-shade",
active={currentIntersection === 0} cIf(isContentPanelAtLeast3xl, "mb-16", "h-[60vh]")
/> )}>
{item.thumbnail?.data?.attributes ? (
{item.gallery && item.gallery.data.length > 0 && ( <Img
<NavOption src={item.thumbnail.data.attributes}
title={langui.gallery} quality={ImageQuality.Large}
url={`#${intersectionIds[1]}`} className="h-full w-full object-contain"
border onClick={() => {
active={currentIntersection === 1} showLightBox([item.thumbnail?.data?.attributes]);
/> }}
)}
<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 className="aspect-[21/29.7] w-full rounded-xl bg-light" />
)} )}
</div> </div>
</SubPanel>
),
[currentIntersection, isVariantSet, item.contents, item.gallery, item.subitems, langui]
);
const contentPanel = useMemo( <InsetBox id={intersectionIds[0]} className="grid place-items-center">
() => ( <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
<ContentPanel width={ContentPanelWidthSizes.Full}> {item.subitem_of?.data[0]?.attributes && (
<ReturnButton <div className="grid place-items-center">
href="/library/" <p>{langui.subitem_of}</p>
title={langui.library} <Button
displayOnlyOn="1ColumnLayout" href={`/library/${item.subitem_of.data[0].attributes.slug}`}
className="mb-10" text={prettyInlineTitle(
/> "",
<div className="grid place-items-center gap-12"> item.subitem_of.data[0].attributes.title,
<div item.subitem_of.data[0].attributes.subtitle
className={cJoin( )}
"relative h-[50vh] w-full cursor-pointer drop-shadow-xl shadow-shade", />
cIf(isContentPanelAtLeast3xl, "mb-16", "h-[60vh]") </div>
)}> )}
{item.thumbnail?.data?.attributes ? ( <div className="grid place-items-center text-center">
<Img <h1 className="text-3xl">{item.title}</h1>
src={item.thumbnail.data.attributes} {isDefinedAndNotEmpty(item.subtitle) && <h2 className="text-2xl">{item.subtitle}</h2>}
quality={ImageQuality.Large} </div>
className="h-full w-full object-contain"
onClick={() => { {!isUntangibleGroupItem(item.metadata?.[0]) && isDefinedAndNotEmpty(itemId) && (
showLightBox([item.thumbnail?.data?.attributes]); <PreviewCardCTAs id={itemId} expand />
}} )}
/>
) : ( {item.descriptions?.[0] && (
<div className="aspect-[21/29.7] w-full rounded-xl bg-light" /> <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> </div>
</InsetBox>
<InsetBox id={intersectionIds[0]} className="grid place-items-center"> {item.gallery && item.gallery.data.length > 0 && (
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> <div id={intersectionIds[1]} className="grid w-full place-items-center gap-8">
{item.subitem_of?.data[0]?.attributes && ( <h2 className="text-2xl">{langui.gallery}</h2>
<div className="grid place-items-center"> <div
<p>{langui.subitem_of}</p> className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
<Button gap-8">
href={`/library/${item.subitem_of.data[0].attributes.slug}`} {filterHasAttributes(item.gallery.data, ["id", "attributes"] as const).map(
text={prettyInlineTitle( (galleryItem, index) => (
"", <Fragment key={galleryItem.id}>
item.subitem_of.data[0].attributes.title, <div
item.subitem_of.data[0].attributes.subtitle 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>
)} )}
<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) && ( {item.release_date && (
<PreviewCardCTAs id={itemId} expand /> <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] && ( {item.price && (
<p className="text-justify">{item.descriptions[0].description}</p> <div className="grid place-content-start place-items-center text-center">
)} <h3 className="text-xl">{langui.price}</h3>
{!( <p>
item.metadata && {prettyPrice(
item.metadata[0]?.__typename === "ComponentMetadataGroup" && item.price,
(item.metadata[0].subtype?.data?.attributes?.slug === "variant-set" || currencies,
item.metadata[0].subtype?.data?.attributes?.slug === "relation-set") item.price.currency?.data?.attributes?.code
) && ( )}
<> </p>
{item.urls?.length ? ( {item.price.currency?.data?.attributes?.code !== currency && (
<div className="flex flex-row place-items-center gap-3"> <p>
<p>{langui.available_at}</p> {prettyPrice(item.price, currencies, currency)} <br />(
{filterHasAttributes(item.urls, ["url"] as const).map((url, index) => ( {langui.calculated?.toLowerCase()})
<Fragment key={index}> </p>
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
</Fragment>
))}
</div>
) : (
<p>{langui.item_not_available}</p>
)} )}
</> </div>
)} )}
</div> </div>
</InsetBox>
{item.gallery && item.gallery.data.length > 0 && ( {item.categories && item.categories.data.length > 0 && (
<div id={intersectionIds[1]} className="grid w-full place-items-center gap-8"> <div className="flex flex-col place-items-center gap-2">
<h2 className="text-2xl">{langui.gallery}</h2> <h3 className="text-xl">{langui.categories}</h3>
<div <div className="flex flex-row flex-wrap place-content-center gap-2">
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end {filterHasAttributes(item.categories.data, ["attributes"] as const).map(
gap-8"> (category) => (
{filterHasAttributes(item.gallery.data, ["id", "attributes"] as const).map( <Chip key={category.id} text={category.attributes.name} />
(galleryItem, index) => ( )
<Fragment key={galleryItem.id}> )}
<div </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>
</div> )}
)}
<InsetBox id={intersectionIds[2]} className="grid place-items-center"> {item.size && (
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-10">
<h2 className="text-center text-2xl">{langui.details}</h2>
<div <div
className={cJoin( className={cJoin(
"grid place-items-center gap-y-8", "grid gap-4",
cIf(isContentPanelAtLeast3xl, "grid-flow-col place-content-between") cIf(!isContentPanelAtLeast3xl, "place-items-center")
)}> )}>
{item.metadata?.[0] && ( <h3 className="text-xl">{langui.size}</h3>
<div className="grid place-content-start place-items-center"> <div
<h3 className="text-xl">{langui.type}</h3> className={cJoin(
<div className="grid grid-flow-col gap-1"> "grid w-full",
<Chip text={prettyItemType(item.metadata[0], langui)} /> cIf(
{""} isContentPanelAtLeastSm,
<Chip text={prettyItemSubType(item.metadata[0])} /> "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> </div>
)} <div
className={cJoin(
{item.release_date && ( "grid gap-x-4",
<div className="grid place-content-start place-items-center"> cIf(
<h3 className="text-xl">{langui.release_date}</h3> isContentPanelAtLeast3xl,
<p>{prettyDate(item.release_date, router.locale)}</p> "grid-flow-col place-items-start",
</div> "place-items-center"
)}
{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} />
) )
)} )}>
<p className="font-bold">{langui.height}:</p>
<div>
<p>{item.size.height} mm</p>
<p>{convertMmToInch(item.size.height)} in</p>
</div>
</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>
)} </div>
)}
{item.size && ( {item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
<div <div
className={cJoin( className={cJoin(
"grid gap-4", "grid gap-4",
cIf(!isContentPanelAtLeast3xl, "place-items-center") cIf(!isContentPanelAtLeast3xl, "place-items-center")
)}> )}>
<h3 className="text-xl">{langui.size}</h3> <h3 className="text-xl">{langui.type_information}</h3>
<div <div className="flex flex-wrap place-content-between gap-x-8">
className={cJoin( {item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
"grid w-full", <>
cIf( <div className="flex flex-row place-content-start gap-4">
isContentPanelAtLeastSm, <p className="font-bold">{langui.pages}:</p>
"grid-flow-col place-content-between", <p>{item.metadata[0].page_count}</p>
"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>
</div> </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> </div>
)} )}
</div>
</InsetBox>
{item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" && {item.subitems && item.subitems.data.length > 0 && (
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && ( <div id={intersectionIds[3]} className="grid w-full place-items-center gap-8">
<div <h2 className="text-2xl">{isVariantSet ? langui.variants : langui.subitems}</h2>
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>
<div className="flex flex-row place-content-start gap-4"> {hoverable && (
<p className="font-bold">{langui.binding}:</p> <WithLabel label={langui.always_show_info}>
<p> <Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
{item.metadata[0].binding_type === </WithLabel>
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"> <div
<p className="font-bold">{langui.page_order}:</p> className="grid w-full grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]
<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))]
items-end gap-8"> items-end gap-8">
{filterHasAttributes(item.subitems.data, ["id", "attributes"] as const).map( {filterHasAttributes(item.subitems.data, ["id", "attributes"] as const).map(
(subitem) => ( (subitem) => (
<Fragment key={subitem.id}> <Fragment key={subitem.id}>
<PreviewCard <PreviewCard
href={`/library/${subitem.attributes.slug}`} href={`/library/${subitem.attributes.slug}`}
title={subitem.attributes.title} title={subitem.attributes.title}
subtitle={subitem.attributes.subtitle} subtitle={subitem.attributes.subtitle}
thumbnail={subitem.attributes.thumbnail?.data?.attributes} thumbnail={subitem.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7" thumbnailAspectRatio="21/29.7"
thumbnailRounded={false} thumbnailRounded={false}
keepInfoVisible={keepInfoVisible} keepInfoVisible={keepInfoVisible}
topChips={ topChips={
subitem.attributes.metadata && subitem.attributes.metadata &&
subitem.attributes.metadata.length > 0 && subitem.attributes.metadata.length > 0 &&
subitem.attributes.metadata[0] subitem.attributes.metadata[0]
? [prettyItemSubType(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
} }
rangeStart={ bottomChips={subitem.attributes.categories?.data.map(
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange" (category) => category.attributes?.short ?? ""
? `${rangedContent.attributes.range[0].starting_page}` )}
: "" 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}
/> />
) </Fragment>
)} )
</div> )}
</div> </div>
)} </div>
</div> )}
</ContentPanel>
), {item.contents && item.contents.data.length > 0 && (
[ <div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
langui, <h2 className="-mb-6 text-2xl">{langui.contents}</h2>
isContentPanelAtLeast3xl, {displayOpenScans && (
item.thumbnail?.data?.attributes, <div className="grid grid-flow-col gap-4">
item.subitem_of?.data, <Button href={`/library/${item.slug}/reader`} text={langui.view_scans} />
item.title, </div>
item.subtitle, )}
item.metadata, <div className="max-w- grid w-full gap-4">
item.descriptions, {filterHasAttributes(item.contents.data, ["attributes"] as const).map(
item.urls, (rangedContent) => (
item.gallery, <ContentLine
item.release_date, content={
item.price, rangedContent.attributes.content?.data?.attributes
item.categories, ? {
item.size, translations: filterDefined(
item.subitems, rangedContent.attributes.content.data.attributes.translations
item.contents, ).map((translation) => ({
item.slug, pre_title: translation.pre_title,
itemId, title: translation.title,
router.locale, subtitle: translation.subtitle,
currencies, language: translation.language?.data?.attributes?.code,
currency, })),
isContentPanelAtLeastSm, categories: filterHasAttributes(
isVariantSet, rangedContent.attributes.content.data.attributes.categories?.data,
hoverable, ["attributes"]
toggleKeepInfoVisible, ).map((category) => category.attributes.short),
keepInfoVisible, type:
displayOpenScans, rangedContent.attributes.content.data.attributes.type?.data
showLightBox, ?.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} />; return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;

View File

@ -1,5 +1,5 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; 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 { useHotkeys } from "react-hotkeys-hook";
import Slider from "rc-slider"; import Slider from "rc-slider";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -44,6 +44,7 @@ import { useFullscreen } from "hooks/useFullscreen";
import { atoms } from "contexts/atoms"; import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms"; import { useAtomGetter } from "helpers/atoms";
import { FilterSettings, useReaderSettings } from "hooks/useReaderSettings"; import { FilterSettings, useReaderSettings } from "hooks/useReaderSettings";
import { useIsWebkit } from "hooks/useIsWebkit";
const CUSTOM_DARK_DROPSHADOW = ` const CUSTOM_DARK_DROPSHADOW = `
drop-shadow(0 0 0.5em rgb(var(--theme-color-shade) / 30%)) drop-shadow(0 0 0.5em rgb(var(--theme-color-shade) / 30%))
@ -112,19 +113,14 @@ const LibrarySlug = ({
is1ColumnLayout ? "single" : "double" is1ColumnLayout ? "single" : "double"
); );
const router = useRouter(); const router = useRouter();
const isWebkit = useIsWebkit();
const { isFullscreen, toggleFullscreen, requestFullscreen } = useFullscreen(Ids.ContentPanel); const { isFullscreen, toggleFullscreen, requestFullscreen } = useFullscreen(Ids.ContentPanel);
const effectiveDisplayMode = useMemo( const effectiveDisplayMode =
() => currentPageIndex === 0 || currentPageIndex === pages.length - 1 ? "single" : displayMode;
currentPageIndex === 0 || currentPageIndex === pages.length - 1 ? "single" : displayMode,
[currentPageIndex, displayMode, pages.length]
);
const ajustedSidepagesTotalWidth = useMemo( const ajustedSidepagesTotalWidth = pages.length * SIDEPAGES_PAGE_WIDTH * (120 / pageWidth);
() => pages.length * SIDEPAGES_PAGE_WIDTH * (120 / pageWidth),
[pageWidth, pages.length]
);
const changeCurrentPageIndex = useCallback( const changeCurrentPageIndex = useCallback(
(callbackFn: (current: number) => number) => { (callbackFn: (current: number) => number) => {
@ -185,61 +181,39 @@ const LibrarySlug = ({
handlePageNavigation, handlePageNavigation,
]); ]);
const firstPage = useMemo( const firstPage =
() => pages[
pages[ effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
effectiveDisplayMode === "double" && currentPageIndex % 2 === 0 ? currentPageIndex - 1
? currentPageIndex - 1 : currentPageIndex
: currentPageIndex ];
],
[currentPageIndex, effectiveDisplayMode, pages]
);
const secondPage = useMemo(
() =>
pages[
effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
? currentPageIndex
: currentPageIndex + 1
],
[currentPageIndex, effectiveDisplayMode, pages]
);
const leftSidePagesCount = useMemo( const secondPage =
() => pages[
pageOrder === PageOrder.LeftToRight ? currentPageIndex : pages.length - 1 - currentPageIndex, effectiveDisplayMode === "double" && currentPageIndex % 2 === 0
[currentPageIndex, pageOrder, pages.length] ? currentPageIndex
); : currentPageIndex + 1
];
const rightSidePagesCount = useMemo( const leftSidePagesCount =
() => pageOrder === PageOrder.LeftToRight ? currentPageIndex : pages.length - 1 - currentPageIndex;
pageOrder === PageOrder.LeftToRight ? pages.length - 1 - currentPageIndex : currentPageIndex,
[currentPageIndex, pageOrder, pages.length]
);
const leftSidePagesWidth = useMemo( const rightSidePagesCount =
() => pageOrder === PageOrder.LeftToRight ? pages.length - 1 - currentPageIndex : currentPageIndex;
`${
pageOrder === PageOrder.LeftToRight
? (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
: ajustedSidepagesTotalWidth -
(currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
}vmin`,
[ajustedSidepagesTotalWidth, currentPageIndex, pageOrder, pages.length]
);
const rightSidePagesWidth = useMemo( const leftSidePagesWidth = `${
() => pageOrder === PageOrder.LeftToRight
`${ ? (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
pageOrder === PageOrder.LeftToRight : ajustedSidepagesTotalWidth - (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
? ajustedSidepagesTotalWidth - }vmin`;
(currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
: (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
}vmin`,
[ajustedSidepagesTotalWidth, currentPageIndex, pageOrder, pages.length]
);
const leftSideClipPath = useMemo( const rightSidePagesWidth = `${
() => `polygon( pageOrder === PageOrder.LeftToRight
? ajustedSidepagesTotalWidth - (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
: (currentPageIndex / pages.length) * ajustedSidepagesTotalWidth
}vmin`;
const leftSideClipPath = `polygon(
${ ${
isSidePagesEnabled isSidePagesEnabled
? ` ? `
@ -265,12 +239,9 @@ const LibrarySlug = ({
: "101% 0%, 101% 100%," : "101% 0%, 101% 100%,"
} }
70% 100% 70% 100%
)`, )`;
[filterSettings.bookFold, isSidePagesEnabled, leftSidePagesWidth]
);
const rightSideClipPath = useMemo( const rightSideClipPath = `polygon(
() => `polygon(
${ ${
isSidePagesEnabled isSidePagesEnabled
? `calc(100% - ${rightSidePagesWidth}) 0%, ? `calc(100% - ${rightSidePagesWidth}) 0%,
@ -295,48 +266,237 @@ const LibrarySlug = ({
: "-1% 100%, -1% 0%," : "-1% 100%, -1% 0%,"
} }
30% 0% 30% 0%
)`, )`;
[filterSettings.bookFold, isSidePagesEnabled, rightSidePagesWidth]
);
const pageHeight = useMemo( const pageHeight = `calc(100vh - ${is1ColumnLayout ? 5 : 4}rem - 3rem)`;
() => `calc(100vh - ${is1ColumnLayout ? 5 : 4}rem - 3rem)`,
[is1ColumnLayout]
);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton title={langui.item} href={`/library/${itemSlug}`} />
<ReturnButton title={langui.item} href={`/library/${itemSlug}`} />
<div className="mt-4 grid gap-2"> <div className="mt-4 grid gap-2">
<WithLabel label={langui.paper_texture}> <WithLabel label={langui.paper_texture}>
<Switch value={filterSettings.paperTexture} onClick={togglePaperTexture} /> <Switch value={filterSettings.paperTexture} onClick={togglePaperTexture} />
</WithLabel> </WithLabel>
<WithLabel label={langui.book_fold}> <WithLabel label={langui.book_fold}>
<Switch value={filterSettings.bookFold} onClick={toggleBookFold} /> <Switch value={filterSettings.bookFold} onClick={toggleBookFold} />
</WithLabel> </WithLabel>
<WithLabel label={langui.lighting}> <WithLabel label={langui.lighting}>
<Switch value={filterSettings.lighting} onClick={toggleLighting} /> <Switch value={filterSettings.lighting} onClick={toggleLighting} />
</WithLabel> </WithLabel>
<WithLabel label={langui.side_pages}> <WithLabel label={langui.side_pages}>
<Switch value={isSidePagesEnabled} onClick={toggleIsSidePagesEnabled} /> <Switch value={isSidePagesEnabled} onClick={toggleIsSidePagesEnabled} />
</WithLabel> </WithLabel>
{!isWebkit && (
<WithLabel label={langui.shadow}> <WithLabel label={langui.shadow}>
<Switch value={filterSettings.dropShadow} onClick={toggleDropShadow} /> <Switch value={filterSettings.dropShadow} onClick={toggleDropShadow} />
</WithLabel> </WithLabel>
</div> )}
</div>
<div className="mt-4 grid"> <div className="mt-4 grid">
<p>{langui.night_reader}:</p> <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 <Slider
reverse={pageOrder === PageOrder.RightToLeft}
min={0} min={0}
max={10} max={pages.length - 1}
value={filterSettings.teint * 10} value={currentPageIndex - 1}
onChange={(event) => { onChange={(event) => {
let value = 0; let value = 0;
if (Array.isArray(event)) { if (Array.isArray(event)) {
@ -344,317 +504,60 @@ const LibrarySlug = ({
} else { } else {
value = event; value = event;
} }
setTeint(value / 10); changeCurrentPageIndex(() => value);
}} }}
/> />
</div> <div className="flex gap-2">
<Button
<div className="mt-8 grid gap-2"> icon={isGalleryMode ? Icon.ExpandMore : Icon.ExpandLess}
<p>{langui.reading_layout}:</p> onClick={() => setIsGalleryMode((current) => !current)}
<ButtonGroup size="small"
buttonsProps={[ />
{ <Button
icon: Icon.Description, icon={isFullscreen ? Icon.FullscreenExit : Icon.Fullscreen}
tooltip: langui.single_page_view, onClick={toggleFullscreen}
active: displayMode === "single", size="small"
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>
</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>
</div> </div>
</ContentPanel> <div className="h-[calc(100vh-4rem)] overflow-y-scroll px-8">
), {item.contents?.data.map((content) => (
[ <Fragment key={content.id}>
is1ColumnLayout, {content.attributes?.scan_set?.[0] && (
currentZoom, <TranslatedScanSet
filterSettings, scanSet={content.attributes.scan_set}
isDarkMode, onClickOnImage={(index) => {
pageHeight, const range = content.attributes?.range[0];
effectiveDisplayMode, let newPageIndex = index + 1;
firstPage, if (range?.__typename === "ComponentRangePageRange") {
pageQuality, newPageIndex += range.starting_page;
bookType, }
leftSideClipPath, changeCurrentPageIndex(() => newPageIndex);
isSidePagesEnabled, setIsGalleryMode(false);
leftSidePagesWidth, }}
leftSidePagesCount, id={content.attributes.slug}
pageOrder, translations={filterHasAttributes(
secondPage, content.attributes.content?.data?.attributes?.translations,
rightSideClipPath, ["language.data.attributes"] as const
rightSidePagesWidth, ).map((translation) => ({
rightSidePagesCount, language: translation.language.data.attributes.code,
isGalleryMode, title: prettyInlineTitle(
currentPageIndex, translation.pre_title,
pages.length, translation.title,
isFullscreen, translation.subtitle
toggleFullscreen, ),
item.contents?.data, }))}
item.slug, fallback={{
handlePageNavigation, title: prettySlug(content.attributes.slug, item.slug),
changeCurrentPageIndex, }}
] content={content.attributes.content}
/>
)}
</Fragment>
))}
</div>
</div>
</ContentPanel>
); );
return ( return (
@ -798,9 +701,9 @@ interface PageFiltersProps {
const PageFilters = ({ page, bookType, options }: PageFiltersProps) => { const PageFilters = ({ page, bookType, options }: PageFiltersProps) => {
const isDarkMode = useAtomGetter(atoms.settings.darkMode); const isDarkMode = useAtomGetter(atoms.settings.darkMode);
const commonCss = useMemo( const commonCss = cJoin(
() => cJoin("absolute inset-0", cIf(page === "right", "[background-position-x:-100%]")), "absolute inset-0",
[page] cIf(page === "right", "[background-position-x:-100%]")
); );
return ( return (
@ -929,10 +832,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
}, []), }, []),
}); });
const pages = useMemo( const pages = filterHasAttributes(selectedScan?.pages?.data, ["attributes"]);
() => filterHasAttributes(selectedScan?.pages?.data, ["attributes"]),
[selectedScan]
);
return ( return (
<> <>

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react"; import { useState, useCallback } from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import naturalCompare from "string-natural-compare"; import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
@ -222,255 +222,222 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
[groupingMethod, langui] [groupingMethod, langui]
); );
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader
<PanelHeader icon={Icon.LibraryBooks}
icon={Icon.LibraryBooks} title={langui.library}
title={langui.library} description={langui.library_description}
description={langui.library_description} />
/>
<HorizontalLine /> <HorizontalLine />
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."} placeholder={langui.search_title ?? "Search..."}
value={searchName} value={searchName}
onChange={(name) => { onChange={(name) => {
setSearchName(name); setSearchName(name);
if (isDefinedAndNotEmpty(name)) { if (isDefinedAndNotEmpty(name)) {
sendAnalytics("Library", "Change search term"); sendAnalytics("Library", "Change search term");
} else { } else {
sendAnalytics("Library", "Clear search term"); sendAnalytics("Library", "Clear search term");
} }
}} }}
/> />
<WithLabel label={langui.group_by}> <WithLabel label={langui.group_by}>
<Select <Select
className="w-full" className="w-full"
options={[ options={[
langui.category ?? "Category", langui.category ?? "Category",
langui.type ?? "Type", langui.type ?? "Type",
langui.release_year ?? "Year", 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),
},
]} ]}
value={groupingMethod}
onChange={(value) => {
setGroupingMethod(value);
sendAnalytics(
"Library",
`Change grouping method (${["none", "category", "type", "year"][value + 1]})`
);
}}
allowEmpty
/> />
</WithLabel>
<Button <WithLabel label={langui.order_by}>
className="mt-8" <Select
text={langui.reset_all_filters} className="w-full"
icon={Icon.Replay} options={[
onClick={() => { langui.name ?? "Name",
setSearchName(DEFAULT_FILTERS_STATE.searchName); langui.price ?? "Price",
setShowSubitems(DEFAULT_FILTERS_STATE.showSubitems); langui.release_date ?? "Release date",
setShowPrimaryItems(DEFAULT_FILTERS_STATE.showPrimaryItems); ]}
setShowSecondaryItems(DEFAULT_FILTERS_STATE.showSecondaryItems); value={sortingMethod}
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod); onChange={(value) => {
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod); setSortingMethod(value);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); sendAnalytics(
setFilterUserStatus(DEFAULT_FILTERS_STATE.filterUserStatus); "Library",
sendAnalytics("Library", "Reset all filters"); `Change sorting method (${["name", "price", "release date"][value]})`
);
}} }}
/> />
</SubPanel> </WithLabel>
),
[ <WithLabel label={langui.show_subitems}>
filterUserStatus, <Switch
groupingMethod, value={showSubitems}
hoverable, onClick={() => {
keepInfoVisible, toggleShowSubitems();
langui, sendAnalytics("Library", `${showSubitems ? "Hide" : "Show"} subitems`);
searchName, }}
setKeepInfoVisible, />
setShowPrimaryItems, </WithLabel>
setShowSecondaryItems,
setShowSubitems, <WithLabel label={langui.show_primary_items}>
showPrimaryItems, <Switch
showSecondaryItems, value={showPrimaryItems}
showSubitems, onClick={() => {
sortingMethod, toggleShowPrimaryItems();
toggleKeepInfoVisible, sendAnalytics("Library", `${showPrimaryItems ? "Hide" : "Show"} primary items`);
toggleShowPrimaryItems, }}
toggleShowSecondaryItems, />
toggleShowSubitems, </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( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList
<SmartList items={filterHasAttributes(items, ["id", "attributes"] as const)}
items={filterHasAttributes(items, ["id", "attributes"] as const)} getItemId={(item) => item.id}
getItemId={(item) => item.id} renderItem={({ item }) => (
renderItem={({ item }) => ( <PreviewCard
<PreviewCard href={`/library/${item.attributes.slug}`}
href={`/library/${item.attributes.slug}`} title={item.attributes.title}
title={item.attributes.title} subtitle={item.attributes.subtitle}
subtitle={item.attributes.subtitle} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="21/29.7"
thumbnailAspectRatio="21/29.7" thumbnailRounded={false}
thumbnailRounded={false} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} topChips={
topChips={ item.attributes.metadata &&
item.attributes.metadata && item.attributes.metadata.length > 0 &&
item.attributes.metadata.length > 0 && item.attributes.metadata[0]
item.attributes.metadata[0] ? [prettyItemSubType(item.attributes.metadata[0])]
? [prettyItemSubType(item.attributes.metadata[0])] : []
: [] }
} bottomChips={item.attributes.categories?.data.map(
bottomChips={item.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} metadata={{
metadata={{ releaseDate: item.attributes.release_date,
releaseDate: item.attributes.release_date, price: item.attributes.price,
price: item.attributes.price, position: "Bottom",
position: "Bottom", }}
}} infoAppend={
infoAppend={ !isUntangibleGroupItem(item.attributes.metadata?.[0]) && (
!isUntangibleGroupItem(item.attributes.metadata?.[0]) && ( <PreviewCardCTAs id={item.id} />
<PreviewCardCTAs id={item.id} /> )
) }
} />
/> )}
)} className={cJoin(
className={cJoin( "grid-cols-2 items-end",
"grid-cols-2 items-end", cIf(isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]")
cIf(isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]") )}
)} searchingTerm={searchName}
searchingTerm={searchName} sortingFunction={sortingFunction}
sortingFunction={sortingFunction} groupingFunction={groupingFunction}
groupingFunction={groupingFunction} searchingBy={(item) =>
searchingBy={(item) => prettyInlineTitle("", item.attributes.title, item.attributes.subtitle)
prettyInlineTitle("", item.attributes.title, item.attributes.subtitle) }
} filteringFunction={filteringFunction}
filteringFunction={filteringFunction} paginationItemPerPage={25}
paginationItemPerPage={25} />
/> </ContentPanel>
</ContentPanel>
),
[
filteringFunction,
groupingFunction,
isContentPanelAtLeast4xl,
items,
keepInfoVisible,
searchName,
sortingFunction,
]
); );
return ( return (

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useMemo, useState } from "react"; import { useState } from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Switch } from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
@ -58,101 +58,95 @@ const News = ({ posts, ...otherProps }: Props): JSX.Element => {
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode); const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader icon={Icon.Feed} title={langui.news} description={langui.news_description} />
<PanelHeader icon={Icon.Feed} title={langui.news} description={langui.news_description} />
<HorizontalLine /> <HorizontalLine />
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."} placeholder={langui.search_title ?? "Search..."}
value={searchName} value={searchName}
onChange={(name) => { onChange={(name) => {
setSearchName(name); setSearchName(name);
if (isDefinedAndNotEmpty(name)) { if (isDefinedAndNotEmpty(name)) {
sendAnalytics("News", "Change search term"); sendAnalytics("News", "Change search term");
} else { } else {
sendAnalytics("News", "Clear search term"); sendAnalytics("News", "Clear search term");
} }
}} }}
/> />
{hoverable && ( {hoverable && (
<WithLabel label={langui.always_show_info}> <WithLabel label={langui.always_show_info}>
<Switch <Switch
value={keepInfoVisible} value={keepInfoVisible}
onClick={() => { onClick={() => {
toggleKeepInfoVisible(); toggleKeepInfoVisible();
sendAnalytics("News", `Always ${keepInfoVisible ? "hide" : "show"} info`); sendAnalytics("News", `Always ${keepInfoVisible ? "hide" : "show"} info`);
}} }}
/> />
</WithLabel> </WithLabel>
)} )}
<Button <Button
className="mt-8" className="mt-8"
text={langui.reset_all_filters} text={langui.reset_all_filters}
icon={Icon.Replay} icon={Icon.Replay}
onClick={() => { onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName); setSearchName(DEFAULT_FILTERS_STATE.searchName);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
sendAnalytics("News", "Reset all filters"); sendAnalytics("News", "Reset all filters");
}} }}
/> />
</SubPanel> </SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName, setKeepInfoVisible, toggleKeepInfoVisible]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList
<SmartList items={filterHasAttributes(posts, ["attributes", "id"] as const)}
items={filterHasAttributes(posts, ["attributes", "id"] as const)} getItemId={(post) => post.id}
getItemId={(post) => post.id} renderItem={({ item: post }) => (
renderItem={({ item: post }) => ( <TranslatedPreviewCard
<TranslatedPreviewCard href={`/news/${post.attributes.slug}`}
href={`/news/${post.attributes.slug}`} translations={filterHasAttributes(post.attributes.translations, [
translations={filterHasAttributes(post.attributes.translations, [ "language.data.attributes.code",
"language.data.attributes.code", ] as const).map((translation) => ({
] as const).map((translation) => ({ language: translation.language.data.attributes.code,
language: translation.language.data.attributes.code, title: translation.title,
title: translation.title, description: translation.excerpt,
description: translation.excerpt, }))}
}))} fallback={{ title: prettySlug(post.attributes.slug) }}
fallback={{ title: prettySlug(post.attributes.slug) }} thumbnail={post.attributes.thumbnail?.data?.attributes}
thumbnail={post.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="3/2"
thumbnailAspectRatio="3/2" thumbnailForceAspectRatio
thumbnailForceAspectRatio bottomChips={post.attributes.categories?.data.map(
bottomChips={post.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} metadata={{
metadata={{ releaseDate: post.attributes.date,
releaseDate: post.attributes.date, releaseDateFormat: "long",
releaseDateFormat: "long", position: "Top",
position: "Top", }}
}} />
/> )}
)} className={cIf(
className={cIf( isContentPanelAtLeast4xl,
isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-x-4 gap-y-6"
"grid-cols-2 gap-x-4 gap-y-6" )}
)} searchingTerm={searchName}
searchingTerm={searchName} searchingBy={(post) =>
searchingBy={(post) => `${prettySlug(post.attributes.slug)} ${post.attributes.translations
`${prettySlug(post.attributes.slug)} ${post.attributes.translations ?.map((translation) => translation?.title)
?.map((translation) => translation?.title) .join(" ")}`
.join(" ")}` }
} paginationItemPerPage={25}
paginationItemPerPage={25} />
/> </ContentPanel>
</ContentPanel>
),
[keepInfoVisible, posts, searchName, isContentPanelAtLeast4xl]
); );
if (isTerminalMode) { if (isTerminalMode) {

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react"; import { useCallback } from "react";
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
@ -49,146 +49,126 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
}); });
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton href={`/wiki`} title={langui.wiki} displayOnlyOn={"3ColumnsLayout"} />
<ReturnButton href={`/wiki`} title={langui.wiki} displayOnlyOn={"3ColumnsLayout"} /> </SubPanel>
</SubPanel>
),
[langui]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Large}>
<ContentPanel width={ContentPanelWidthSizes.Large}> <ReturnButton
<ReturnButton href={`/wiki`}
href={`/wiki`} title={langui.wiki}
title={langui.wiki} displayOnlyOn={"1ColumnLayout"}
displayOnlyOn={"1ColumnLayout"} className="mb-10"
className="mb-10" />
/>
<div className="flex flex-wrap place-content-center gap-3"> <div className="flex flex-wrap place-content-center gap-3">
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1> <h1 className="text-center text-3xl">{selectedTranslation?.title}</h1>
{selectedTranslation?.aliases && selectedTranslation.aliases.length > 0 && ( {selectedTranslation?.aliases && selectedTranslation.aliases.length > 0 && (
<p className="mr-3 text-center text-2xl"> <p className="mr-3 text-center text-2xl">
{`(${selectedTranslation.aliases.map((alias) => alias?.alias).join("・")})`} {`(${selectedTranslation.aliases.map((alias) => alias?.alias).join("・")})`}
</p> </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>
</>
)} )}
</ContentPanel> <LanguageSwitcher {...languageSwitcherProps} />
), </div>
[
LanguageSwitcher, {selectedTranslation && (
is3ColumnsLayout, <>
languageSwitcherProps, <HorizontalLine />
langui.categories, <div className="text-justify">
langui.summary, <div
langui.tags, className={cJoin(
langui.wiki, "mb-8 overflow-hidden rounded-lg bg-mid text-center",
page.categories?.data, cIf(is3ColumnsLayout, "float-right ml-8 w-96")
page.definitions, )}>
page.tags?.data, {page.thumbnail?.data?.attributes && (
page.thumbnail?.data?.attributes, <Img
selectedTranslation, src={page.thumbnail.data.attributes}
showLightBox, 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) { if (isTerminalMode) {

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { Fragment, useCallback, useMemo } from "react"; import { Fragment, useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { InsetBox } from "components/Containers/InsetBox"; import { InsetBox } from "components/Containers/InsetBox";
@ -46,80 +46,70 @@ interface Props extends AppLayoutRequired {
const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props): JSX.Element => { const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
const ids = useMemo( const ids = filterHasAttributes(chronologyEras, ["attributes"] as const).map(
() => (era) => era.attributes.slug
filterHasAttributes(chronologyEras, ["attributes"] as const).map(
(era) => era.attributes.slug
),
[chronologyEras]
); );
const currentIntersection = useIntersectionList(ids); const currentIntersection = useIntersectionList(ids);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <ReturnButton href="/wiki" title={langui.wiki} displayOnlyOn="3ColumnsLayout" />
<ReturnButton href="/wiki" title={langui.wiki} displayOnlyOn="3ColumnsLayout" />
<HorizontalLine /> <HorizontalLine />
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map((era, index) => ( {filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map((era, index) => (
<Fragment key={era.id}> <Fragment key={era.id}>
<TranslatedNavOption <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}
translations={filterHasAttributes(era.attributes.title, [ translations={filterHasAttributes(era.attributes.title, [
"language.data.attributes.code", "language.data.attributes.code",
] as const).map((translation) => ({ ] as const).map((translation) => ({
language: translation.language.data.attributes.code, language: translation.language.data.attributes.code,
title: translation.title, title: translation.title,
description: translation.description, subtitle: `${era.attributes.starting_year}${era.attributes.ending_year}`,
}))} }))}
fallback={{ title: prettySlug(era.attributes.slug) }} fallback={{
chronologyItems={filterHasAttributes(chronologyItems, ["attributes"] as const).filter( title: prettySlug(era.attributes.slug),
(item) => subtitle: `${era.attributes.starting_year}${era.attributes.ending_year}`,
item.attributes.year >= era.attributes.starting_year && }}
item.attributes.year < era.attributes.ending_year url={`#${era.attributes.slug}`}
)} border
active={currentIntersection === index}
/> />
))} </Fragment>
</ContentPanel> ))}
), </SubPanel>
[chronologyEras, chronologyItems, langui] );
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} />; return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
@ -161,7 +151,7 @@ interface ChronologyEraProps {
} }
const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEraProps) => { const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEraProps) => {
const yearGroups = useMemo(() => { const yearGroups = (() => {
const memo: Props["chronologyItems"][] = []; const memo: Props["chronologyItems"][] = [];
let currentYear = -Infinity; let currentYear = -Infinity;
filterHasAttributes(chronologyItems, ["attributes"] as const).forEach((item) => { filterHasAttributes(chronologyItems, ["attributes"] as const).forEach((item) => {
@ -173,7 +163,7 @@ const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEr
} }
}); });
return memo; return memo;
}, [chronologyItems]); })();
return ( return (
<div id={id}> <div id={id}>

View File

@ -1,5 +1,5 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useState } from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { NavOption } from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
@ -67,84 +67,73 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
setValue: setKeepInfoVisible, setValue: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const subPanel = useMemo( const subPanel = (
() => ( <SubPanel>
<SubPanel> <PanelHeader
<PanelHeader icon={Icon.TravelExplore}
icon={Icon.TravelExplore} title={langui.wiki}
title={langui.wiki} description={langui.wiki_description}
description={langui.wiki_description} />
/>
<HorizontalLine /> <HorizontalLine />
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."} placeholder={langui.search_title ?? "Search..."}
value={searchName} value={searchName}
onChange={(name) => { onChange={(name) => {
setSearchName(name); setSearchName(name);
if (isDefinedAndNotEmpty(name)) { if (isDefinedAndNotEmpty(name)) {
sendAnalytics("Wiki", "Change search term"); sendAnalytics("Wiki", "Change search term");
} else { } else {
sendAnalytics("Wiki", "Clear search term"); 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}> {hoverable && (
<Select <WithLabel label={langui.always_show_info}>
className="w-full" <Switch
options={[langui.category ?? "Category"]} value={keepInfoVisible}
value={groupingMethod} onClick={() => {
onChange={(value) => { toggleKeepInfoVisible();
setGroupingMethod(value); sendAnalytics("Wiki", `Always ${keepInfoVisible ? "hide" : "show"} info`);
sendAnalytics("Wiki", `Change grouping method (${["none", "category"][value + 1]})`);
}} }}
allowEmpty
/> />
</WithLabel> </WithLabel>
)}
{hoverable && ( <Button
<WithLabel label={langui.always_show_info}> className="mt-8"
<Switch text={langui.reset_all_filters}
value={keepInfoVisible} icon={Icon.Replay}
onClick={() => { onClick={() => {
toggleKeepInfoVisible(); setSearchName(DEFAULT_FILTERS_STATE.searchName);
sendAnalytics("Wiki", `Always ${keepInfoVisible ? "hide" : "show"} info`); setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
}} setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
/> sendAnalytics("Wiki", "Reset all filters");
</WithLabel> }}
)} />
<Button <HorizontalLine />
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 /> <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>
<NavOption title={langui.chronology} url="/wiki/chronology" border />
</SubPanel>
),
[
groupingMethod,
hoverable,
keepInfoVisible,
langui,
searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
]
); );
const groupingFunction = useCallback( const groupingFunction = useCallback(
@ -172,64 +161,59 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
[groupingMethod, langui] [groupingMethod, langui]
); );
const contentPanel = useMemo( const contentPanel = (
() => ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList
<SmartList items={filterHasAttributes(pages, ["id", "attributes"] as const)}
items={filterHasAttributes(pages, ["id", "attributes"] as const)} getItemId={(item) => item.id}
getItemId={(item) => item.id} renderItem={({ item }) => (
renderItem={({ item }) => ( <TranslatedPreviewCard
<TranslatedPreviewCard href={`/wiki/${item.attributes.slug}`}
href={`/wiki/${item.attributes.slug}`} translations={filterHasAttributes(item.attributes.translations, [
translations={filterHasAttributes(item.attributes.translations, [ "language.data.attributes.code",
"language.data.attributes.code", ] as const).map((translation) => ({
] as const).map((translation) => ({ title: translation.title,
title: translation.title, subtitle:
subtitle: translation.aliases && translation.aliases.length > 0
translation.aliases && translation.aliases.length > 0 ? translation.aliases.map((alias) => alias?.alias).join("・")
? translation.aliases.map((alias) => alias?.alias).join("・") : undefined,
: undefined, description: translation.summary,
description: translation.summary, language: translation.language.data.attributes.code,
language: translation.language.data.attributes.code, }))}
}))} fallback={{ title: prettySlug(item.attributes.slug) }}
fallback={{ title: prettySlug(item.attributes.slug) }} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio={"4/3"}
thumbnailAspectRatio={"4/3"} thumbnailRounded
thumbnailRounded thumbnailForceAspectRatio
thumbnailForceAspectRatio keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} topChips={filterHasAttributes(item.attributes.tags?.data, ["attributes"] as const).map(
topChips={filterHasAttributes(item.attributes.tags?.data, [ (tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
"attributes", )}
] as const).map( bottomChips={filterHasAttributes(item.attributes.categories?.data, [
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug) "attributes",
)} ] as const).map((category) => category.attributes.short)}
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",
className={cIf( "grid-cols-2 gap-x-3 gap-y-5"
isContentPanelAtLeast4xl, )}
"grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-x-6 gap-y-8", searchingTerm={searchName}
"grid-cols-2 gap-x-3 gap-y-5" searchingBy={(item) =>
)} filterDefined(item.attributes.translations)
searchingTerm={searchName} .map(
searchingBy={(item) => (translation) =>
filterDefined(item.attributes.translations) `${translation.title} ${filterDefined(translation.aliases)
.map( .map((alias) => alias.alias)
(translation) => .join(" ")}`
`${translation.title} ${filterDefined(translation.aliases) )
.map((alias) => alias.alias) .join(" ")
.join(" ")}` }
) groupingFunction={groupingFunction}
.join(" ") paginationItemPerPage={25}
} />
groupingFunction={groupingFunction} </ContentPanel>
paginationItemPerPage={25}
/>
</ContentPanel>
),
[groupingFunction, keepInfoVisible, pages, searchName, isContentPanelAtLeast4xl]
); );
if (isTerminalMode) { if (isTerminalMode) {

View File

@ -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 */ /* Add support for break-wrods CSS attribute */
plugin(({ addUtilities }) => { plugin(({ addUtilities }) => {
addUtilities({ addUtilities({