Continued using hooks
This commit is contained in:
parent
efcf01e8a0
commit
d0b91f9db6
|
@ -161,30 +161,30 @@ export function AppLayout(props: Props): JSX.Element {
|
|||
}, [fontSize]);
|
||||
|
||||
const defaultPreferredLanguages = useMemo(() => {
|
||||
let list: string[] = [];
|
||||
let memo: string[] = [];
|
||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||
if (router.locale === "en") {
|
||||
list = [router.locale];
|
||||
memo = [router.locale];
|
||||
router.locales.map((locale) => {
|
||||
if (locale !== router.locale) list.push(locale);
|
||||
if (locale !== router.locale) memo.push(locale);
|
||||
});
|
||||
} else {
|
||||
list = [router.locale, "en"];
|
||||
memo = [router.locale, "en"];
|
||||
router.locales.map((locale) => {
|
||||
if (locale !== router.locale && locale !== "en") list.push(locale);
|
||||
if (locale !== router.locale && locale !== "en") memo.push(locale);
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
return memo;
|
||||
}, [router.locale, router.locales]);
|
||||
|
||||
const currencyOptions = useMemo(() => {
|
||||
const list: string[] = [];
|
||||
const memo: string[] = [];
|
||||
filterHasAttributes(currencies).map((currentCurrency) => {
|
||||
if (isDefinedAndNotEmpty(currentCurrency.attributes.code))
|
||||
list.push(currentCurrency.attributes.code);
|
||||
memo.push(currentCurrency.attributes.code);
|
||||
});
|
||||
return list;
|
||||
return memo;
|
||||
}, [currencies]);
|
||||
|
||||
const [currencySelect, setCurrencySelect] = useState<number>(-1);
|
||||
|
|
|
@ -157,12 +157,10 @@ export function ScanSet(props: Props): JSX.Element {
|
|||
{filterHasAttributes(selectedScan.cleaners.data).map(
|
||||
(cleaner) => (
|
||||
<Fragment key={cleaner.id}>
|
||||
{cleaner.attributes && (
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={cleaner.attributes}
|
||||
/>
|
||||
)}
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={cleaner.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
|
@ -178,12 +176,10 @@ export function ScanSet(props: Props): JSX.Element {
|
|||
{filterHasAttributes(selectedScan.typesetters.data).map(
|
||||
(typesetter) => (
|
||||
<Fragment key={typesetter.id}>
|
||||
{typesetter.attributes && (
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={typesetter.attributes}
|
||||
/>
|
||||
)}
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={typesetter.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
|
@ -218,9 +214,7 @@ export function ScanSet(props: Props): JSX.Element {
|
|||
openLightBox(images, index);
|
||||
}}
|
||||
>
|
||||
{page.attributes && (
|
||||
<Img image={page.attributes} quality={ImageQuality.Small} />
|
||||
)}
|
||||
<Img image={page.attributes} quality={ImageQuality.Small} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { getAssetURL, ImageQuality } from "helpers/img";
|
|||
import { filterHasAttributes, getStatusDescription } from "helpers/others";
|
||||
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useMemo } from "react";
|
||||
|
||||
interface Props {
|
||||
openLightBox: (images: string[], index?: number) => void;
|
||||
|
@ -35,19 +35,22 @@ export function ScanSetCover(props: Props): JSX.Element {
|
|||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||
});
|
||||
|
||||
const coverImages: UploadImageFragment[] = [];
|
||||
if (selectedScan?.obi_belt?.full?.data?.attributes)
|
||||
coverImages.push(selectedScan.obi_belt.full.data.attributes);
|
||||
if (selectedScan?.obi_belt?.inside_full?.data?.attributes)
|
||||
coverImages.push(selectedScan.obi_belt.inside_full.data.attributes);
|
||||
if (selectedScan?.dust_jacket?.full?.data?.attributes)
|
||||
coverImages.push(selectedScan.dust_jacket.full.data.attributes);
|
||||
if (selectedScan?.dust_jacket?.inside_full?.data?.attributes)
|
||||
coverImages.push(selectedScan.dust_jacket.inside_full.data.attributes);
|
||||
if (selectedScan?.cover?.full?.data?.attributes)
|
||||
coverImages.push(selectedScan.cover.full.data.attributes);
|
||||
if (selectedScan?.cover?.inside_full?.data?.attributes)
|
||||
coverImages.push(selectedScan.cover.inside_full.data.attributes);
|
||||
const coverImages = useMemo(() => {
|
||||
const memo: UploadImageFragment[] = [];
|
||||
if (selectedScan?.obi_belt?.full?.data?.attributes)
|
||||
memo.push(selectedScan.obi_belt.full.data.attributes);
|
||||
if (selectedScan?.obi_belt?.inside_full?.data?.attributes)
|
||||
memo.push(selectedScan.obi_belt.inside_full.data.attributes);
|
||||
if (selectedScan?.dust_jacket?.full?.data?.attributes)
|
||||
memo.push(selectedScan.dust_jacket.full.data.attributes);
|
||||
if (selectedScan?.dust_jacket?.inside_full?.data?.attributes)
|
||||
memo.push(selectedScan.dust_jacket.inside_full.data.attributes);
|
||||
if (selectedScan?.cover?.full?.data?.attributes)
|
||||
memo.push(selectedScan.cover.full.data.attributes);
|
||||
if (selectedScan?.cover?.inside_full?.data?.attributes)
|
||||
memo.push(selectedScan.cover.inside_full.data.attributes);
|
||||
return memo;
|
||||
}, [selectedScan]);
|
||||
|
||||
if (coverImages.length > 0) {
|
||||
return (
|
||||
|
|
|
@ -67,105 +67,136 @@ export function PostPage(props: Props): JSX.Element {
|
|||
[post.slug, post.thumbnail, selectedTranslation]
|
||||
);
|
||||
|
||||
const subPanel =
|
||||
returnHref || returnTitle || displayCredits || displayToc ? (
|
||||
<SubPanel>
|
||||
const subPanel = useMemo(
|
||||
() =>
|
||||
returnHref || returnTitle || displayCredits || displayToc ? (
|
||||
<SubPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayCredits && (
|
||||
<>
|
||||
{selectedTranslation && (
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(
|
||||
selectedTranslation.status,
|
||||
langui
|
||||
)}
|
||||
maxWidth={"20rem"}
|
||||
>
|
||||
<Chip>{selectedTranslation.status}</Chip>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.authors && post.authors.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{"Authors"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(post.authors.data).map((author) => (
|
||||
<Fragment key={author.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={author.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<HorizontalLine />
|
||||
</>
|
||||
)}
|
||||
|
||||
{displayToc && <TOC text={body} title={title} />}
|
||||
</SubPanel>
|
||||
) : undefined,
|
||||
[
|
||||
body,
|
||||
displayCredits,
|
||||
displayToc,
|
||||
langui,
|
||||
post.authors,
|
||||
returnHref,
|
||||
returnTitle,
|
||||
selectedTranslation,
|
||||
title,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
horizontalLine
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayCredits && (
|
||||
{displayThumbnailHeader ? (
|
||||
<>
|
||||
{selectedTranslation && (
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(
|
||||
selectedTranslation.status,
|
||||
langui
|
||||
)}
|
||||
maxWidth={"20rem"}
|
||||
>
|
||||
<Chip>{selectedTranslation.status}</Chip>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.authors && post.authors.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{"Authors"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(post.authors.data).map((author) => (
|
||||
<Fragment key={author.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={author.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ThumbnailHeader
|
||||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
langui={langui}
|
||||
categories={post.categories}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLanguageSwitcher && (
|
||||
<div className="grid place-content-end place-items-start">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
)}
|
||||
{displayTitle && (
|
||||
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{displayToc && <TOC text={body} title={title} />}
|
||||
</SubPanel>
|
||||
) : undefined;
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
{returnHref && returnTitle && (
|
||||
<ReturnButton
|
||||
href={returnHref}
|
||||
title={returnTitle}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
horizontalLine
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayThumbnailHeader ? (
|
||||
<>
|
||||
<ThumbnailHeader
|
||||
thumbnail={thumbnail}
|
||||
title={title}
|
||||
description={excerpt}
|
||||
langui={langui}
|
||||
categories={post.categories}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLanguageSwitcher && (
|
||||
<div className="grid place-content-end place-items-start">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
)}
|
||||
{displayTitle && (
|
||||
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{prependBody}
|
||||
<Markdawn text={body} />
|
||||
{appendBody}
|
||||
</ContentPanel>
|
||||
{prependBody}
|
||||
<Markdawn text={body} />
|
||||
{appendBody}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
appendBody,
|
||||
body,
|
||||
displayLanguageSwitcher,
|
||||
displayThumbnailHeader,
|
||||
displayTitle,
|
||||
excerpt,
|
||||
langui,
|
||||
post.categories,
|
||||
prependBody,
|
||||
returnHref,
|
||||
returnTitle,
|
||||
thumbnail,
|
||||
title,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -85,7 +85,6 @@ export function ChronologyItemComponent(props: Props): JSX.Element {
|
|||
{translation.description && (
|
||||
<p
|
||||
className={
|
||||
event.translations &&
|
||||
event.translations.length > 1
|
||||
? `mt-2 whitespace-pre-line before:ml-[-1em] before:inline-block
|
||||
before:w-4 before:text-dark before:content-['-']`
|
||||
|
|
|
@ -208,7 +208,7 @@ export function sortBy(
|
|||
orderByType: number,
|
||||
items: Items,
|
||||
currencies: AppStaticProps["currencies"]
|
||||
): Items {
|
||||
) {
|
||||
switch (orderByType) {
|
||||
case 0:
|
||||
return items.sort((a, b) => {
|
||||
|
|
|
@ -19,23 +19,18 @@ type SortContentProps =
|
|||
>["contents"];
|
||||
|
||||
export function sortContent(contents: SortContentProps) {
|
||||
if (contents) {
|
||||
const newContent = { ...contents };
|
||||
newContent?.data.sort((a, b) => {
|
||||
if (
|
||||
a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
|
||||
b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
|
||||
) {
|
||||
return (
|
||||
a.attributes.range[0].starting_page -
|
||||
b.attributes.range[0].starting_page
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return newContent;
|
||||
}
|
||||
return contents;
|
||||
contents?.data.sort((a, b) => {
|
||||
if (
|
||||
a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
|
||||
b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
|
||||
) {
|
||||
return (
|
||||
a.attributes.range[0].starting_page -
|
||||
b.attributes.range[0].starting_page
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function getStatusDescription(
|
||||
|
|
|
@ -38,12 +38,12 @@ export function useSmartLanguage<T>(
|
|||
const router = useRouter();
|
||||
|
||||
const availableLocales = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
const memo = new Map<string, number>();
|
||||
filterDefined(items).map((elem, index) => {
|
||||
const result = languageExtractor(elem);
|
||||
if (isDefined(result)) map.set(result, index);
|
||||
if (isDefined(result)) memo.set(result, index);
|
||||
});
|
||||
return map;
|
||||
return memo;
|
||||
}, [items, languageExtractor]);
|
||||
|
||||
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<
|
||||
|
|
|
@ -7,21 +7,25 @@ import { ContentPanel } from "components/Panels/ContentPanel";
|
|||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {}
|
||||
|
||||
export default function FourOhFour(props: Props): JSX.Element {
|
||||
const { langui } = props;
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<h1>404 - {langui.page_not_found}</h1>
|
||||
<ReturnButton
|
||||
href="/"
|
||||
title="Home"
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Both}
|
||||
/>
|
||||
</ContentPanel>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<h1>404 - {langui.page_not_found}</h1>
|
||||
<ReturnButton
|
||||
href="/"
|
||||
title="Home"
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Both}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
return <AppLayout navTitle="404" contentPanel={contentPanel} {...props} />;
|
||||
}
|
||||
|
|
|
@ -7,21 +7,25 @@ import { ContentPanel } from "components/Panels/ContentPanel";
|
|||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {}
|
||||
|
||||
export default function FiveHundred(props: Props): JSX.Element {
|
||||
const { langui } = props;
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<h1>500 - Internal Server Error</h1>
|
||||
<ReturnButton
|
||||
href="/"
|
||||
title="Home"
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Both}
|
||||
/>
|
||||
</ContentPanel>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<h1>500 - Internal Server Error</h1>
|
||||
<ReturnButton
|
||||
href="/"
|
||||
title="Home"
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Both}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
return <AppLayout navTitle="500" contentPanel={contentPanel} {...props} />;
|
||||
}
|
||||
|
|
|
@ -6,32 +6,35 @@ import { SubPanel } from "components/Panels/SubPanel";
|
|||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {}
|
||||
|
||||
export default function AboutUs(props: Props): JSX.Element {
|
||||
const { langui } = props;
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Info}
|
||||
title={langui.about_us}
|
||||
description={langui.about_us_description}
|
||||
/>
|
||||
<NavOption
|
||||
title={langui.accords_handbook}
|
||||
url="/about-us/accords-handbook"
|
||||
border
|
||||
/>
|
||||
<NavOption title={langui.legality} url="/about-us/legality" border />
|
||||
{/* <NavOption title={langui.members} url="/about-us/members" border /> */}
|
||||
<NavOption
|
||||
title={langui.sharing_policy}
|
||||
url="/about-us/sharing-policy"
|
||||
border
|
||||
/>
|
||||
<NavOption title={langui.contact_us} url="/about-us/contact" border />
|
||||
</SubPanel>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Info}
|
||||
title={langui.about_us}
|
||||
description={langui.about_us_description}
|
||||
/>
|
||||
<NavOption
|
||||
title={langui.accords_handbook}
|
||||
url="/about-us/accords-handbook"
|
||||
border
|
||||
/>
|
||||
<NavOption title={langui.legality} url="/about-us/legality" border />
|
||||
<NavOption
|
||||
title={langui.sharing_policy}
|
||||
url="/about-us/sharing-policy"
|
||||
border
|
||||
/>
|
||||
<NavOption title={langui.contact_us} url="/about-us/contact" border />
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
return (
|
||||
<AppLayout navTitle={langui.about_us} subPanel={subPanel} {...props} />
|
||||
|
|
|
@ -6,20 +6,24 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
|||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { Icon } from "components/Ico";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {}
|
||||
|
||||
export default function Archives(props: Props): JSX.Element {
|
||||
const { langui } = props;
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Inventory}
|
||||
title={langui.archives}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
<NavOption title={"Videos"} url="/archives/videos/" border />
|
||||
</SubPanel>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Inventory}
|
||||
title={langui.archives}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
<NavOption title={"Videos"} url="/archives/videos/" border />
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
return (
|
||||
<AppLayout navTitle={langui.archives} subPanel={subPanel} {...props} />
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
GetStaticPathsResult,
|
||||
GetStaticPropsContext,
|
||||
} from "next";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, useState, useMemo } from "react";
|
||||
import { Icon } from "components/Ico";
|
||||
import { useMediaHoverable } from "hooks/useMediaQuery";
|
||||
import { WithLabel } from "components/Inputs/WithLabel";
|
||||
|
@ -37,67 +37,79 @@ export default function Channel(props: Props): JSX.Element {
|
|||
const [keepInfoVisible, setKeepInfoVisible] = useState(true);
|
||||
const hoverable = useMediaHoverable();
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<PanelHeader
|
||||
icon={Icon.Movie}
|
||||
title={langui.videos}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
className="mb-10"
|
||||
/>
|
||||
)}
|
||||
</SubPanel>
|
||||
|
||||
<PanelHeader
|
||||
icon={Icon.Movie}
|
||||
title={langui.videos}
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl">{channel?.title}</h1>
|
||||
<p>{channel?.subscribers.toLocaleString()} subscribers</p>
|
||||
</div>
|
||||
<div
|
||||
className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl">{channel?.title}</h1>
|
||||
<p>{channel?.subscribers.toLocaleString()} subscribers</p>
|
||||
</div>
|
||||
<div
|
||||
className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2"
|
||||
>
|
||||
{filterHasAttributes(channel?.videos?.data).map((video) => (
|
||||
<Fragment key={video.id}>
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${video.attributes.uid}`}
|
||||
title={video.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(video.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
release_date: video.attributes.published_date,
|
||||
views: video.attributes.views,
|
||||
author: channel?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: video.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
>
|
||||
{filterHasAttributes(channel?.videos?.data).map((video) => (
|
||||
<Fragment key={video.id}>
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${video.attributes.uid}`}
|
||||
title={video.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(video.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
release_date: video.attributes.published_date,
|
||||
views: video.attributes.views,
|
||||
author: channel?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: video.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
channel?.subscribers,
|
||||
channel?.title,
|
||||
channel?.videos?.data,
|
||||
keepInfoVisible,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle={langui.archives}
|
||||
|
|
|
@ -22,110 +22,108 @@ import { filterHasAttributes } from "helpers/others";
|
|||
import { getVideoThumbnailURL } from "helpers/videos";
|
||||
import { useMediaHoverable } from "hooks/useMediaQuery";
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"];
|
||||
}
|
||||
|
||||
const ITEM_PER_PAGE = 50;
|
||||
|
||||
export default function Videos(props: Props): JSX.Element {
|
||||
const { langui, videos } = props;
|
||||
const hoverable = useMediaHoverable();
|
||||
|
||||
videos
|
||||
.sort((a, b) => {
|
||||
const dateA = a.attributes?.published_date
|
||||
? prettyDate(a.attributes.published_date)
|
||||
: "9999";
|
||||
const dateB = b.attributes?.published_date
|
||||
? prettyDate(b.attributes.published_date)
|
||||
: "9999";
|
||||
return dateA.localeCompare(dateB);
|
||||
})
|
||||
.reverse();
|
||||
|
||||
const itemPerPage = 50;
|
||||
const paginatedVideos: Props["videos"][] = [];
|
||||
for (let index = 0; itemPerPage * index < videos.length; index += 1) {
|
||||
paginatedVideos.push(
|
||||
videos.slice(index * itemPerPage, (index + 1) * itemPerPage)
|
||||
);
|
||||
}
|
||||
const paginatedVideos = useMemo(() => {
|
||||
const memo = [];
|
||||
for (let index = 0; ITEM_PER_PAGE * index < videos.length; index += 1) {
|
||||
memo.push(
|
||||
videos.slice(index * ITEM_PER_PAGE, (index + 1) * ITEM_PER_PAGE)
|
||||
);
|
||||
}
|
||||
return memo;
|
||||
}, [videos]);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [keepInfoVisible, setKeepInfoVisible] = useState(true);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/"
|
||||
title={"Archives"}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<PanelHeader
|
||||
icon={Icon.Movie}
|
||||
title="Videos"
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/"
|
||||
title={"Archives"}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
className="mb-10"
|
||||
/>
|
||||
)}
|
||||
</SubPanel>
|
||||
|
||||
<PanelHeader
|
||||
icon={Icon.Movie}
|
||||
title="Videos"
|
||||
description={langui.archives_description}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<PageSelector
|
||||
maxPage={Math.floor(videos.length / itemPerPage)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
className="mb-12"
|
||||
/>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<PageSelector
|
||||
maxPage={Math.floor(videos.length / ITEM_PER_PAGE)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
className="mb-12"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0
|
||||
<div
|
||||
className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2
|
||||
thin:grid-cols-1"
|
||||
>
|
||||
{filterHasAttributes(paginatedVideos[page]).map((video) => (
|
||||
<Fragment key={video.id}>
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${video.attributes.uid}`}
|
||||
title={video.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(video.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
release_date: video.attributes.published_date,
|
||||
views: video.attributes.views,
|
||||
author: video.attributes.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: video.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
>
|
||||
{filterHasAttributes(paginatedVideos[page]).map((video) => (
|
||||
<Fragment key={video.id}>
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${video.attributes.uid}`}
|
||||
title={video.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(video.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
release_date: video.attributes.published_date,
|
||||
views: video.attributes.views,
|
||||
author: video.attributes.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: video.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<PageSelector
|
||||
maxPage={Math.floor(videos.length / itemPerPage)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
className="mt-12"
|
||||
/>
|
||||
</ContentPanel>
|
||||
<PageSelector
|
||||
maxPage={Math.floor(videos.length / ITEM_PER_PAGE)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
className="mt-12"
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[keepInfoVisible, page, paginatedVideos, videos.length]
|
||||
);
|
||||
return (
|
||||
<AppLayout
|
||||
|
@ -143,6 +141,17 @@ export async function getStaticProps(
|
|||
const sdk = getReadySdk();
|
||||
const videos = await sdk.getVideosPreview();
|
||||
if (!videos.videos) return { notFound: true };
|
||||
videos.videos.data
|
||||
.sort((a, b) => {
|
||||
const dateA = a.attributes?.published_date
|
||||
? prettyDate(a.attributes.published_date)
|
||||
: "9999";
|
||||
const dateB = b.attributes?.published_date
|
||||
? prettyDate(b.attributes.published_date)
|
||||
: "9999";
|
||||
return dateA.localeCompare(dateB);
|
||||
})
|
||||
.reverse();
|
||||
const props: Props = {
|
||||
...(await getAppStaticProps(context)),
|
||||
videos: videos.videos.data,
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
GetStaticPathsResult,
|
||||
GetStaticPropsContext,
|
||||
} from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
video: NonNullable<
|
||||
|
@ -37,145 +38,164 @@ export default function Video(props: Props): JSX.Element {
|
|||
const { langui, video } = props;
|
||||
const isMobile = useMediaMobile();
|
||||
const appLayout = useAppLayout();
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
className="mb-10"
|
||||
/>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/archives/videos/"
|
||||
title={langui.videos}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<HorizontalLine />
|
||||
|
||||
<NavOption
|
||||
title={langui.video}
|
||||
url="#video"
|
||||
border
|
||||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||||
/>
|
||||
<NavOption
|
||||
title={langui.video}
|
||||
url="#video"
|
||||
border
|
||||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
title={langui.channel}
|
||||
url="#channel"
|
||||
border
|
||||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||||
/>
|
||||
<NavOption
|
||||
title={langui.channel}
|
||||
url="#channel"
|
||||
border
|
||||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||||
/>
|
||||
|
||||
<NavOption
|
||||
title={langui.description}
|
||||
url="#description"
|
||||
border
|
||||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||||
/>
|
||||
</SubPanel>
|
||||
<NavOption
|
||||
title={langui.description}
|
||||
url="#description"
|
||||
border
|
||||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[appLayout, langui]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
id="video"
|
||||
className="w-full overflow-hidden rounded-xl shadow-lg shadow-shade"
|
||||
>
|
||||
{video.gone ? (
|
||||
<video
|
||||
className="w-full"
|
||||
src={getVideoFile(video.uid)}
|
||||
controls
|
||||
></video>
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
||||
className="aspect-video w-full"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write;
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
id="video"
|
||||
className="w-full overflow-hidden rounded-xl shadow-lg shadow-shade"
|
||||
>
|
||||
{video.gone ? (
|
||||
<video
|
||||
className="w-full"
|
||||
src={getVideoFile(video.uid)}
|
||||
controls
|
||||
></video>
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
|
||||
className="aspect-video w-full"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write;
|
||||
encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
)}
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
)}
|
||||
|
||||
<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)}
|
||||
</p>
|
||||
<p>
|
||||
<Ico
|
||||
icon={Icon.Visibility}
|
||||
className="mr-1 translate-y-[.15em] !text-base"
|
||||
/>
|
||||
{isMobile
|
||||
? prettyShortenNumber(video.views)
|
||||
: video.views.toLocaleString()}
|
||||
</p>
|
||||
{video.channel?.data?.attributes && (
|
||||
<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.ThumbUp}
|
||||
icon={Icon.Event}
|
||||
className="mr-1 translate-y-[.15em] !text-base"
|
||||
/>
|
||||
{prettyDate(video.published_date)}
|
||||
</p>
|
||||
<p>
|
||||
<Ico
|
||||
icon={Icon.Visibility}
|
||||
className="mr-1 translate-y-[.15em] !text-base"
|
||||
/>
|
||||
{isMobile
|
||||
? prettyShortenNumber(video.likes)
|
||||
: video.likes.toLocaleString()}
|
||||
</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()}`}
|
||||
? prettyShortenNumber(video.views)
|
||||
: video.views.toLocaleString()}
|
||||
</p>
|
||||
{video.channel?.data?.attributes && (
|
||||
<p>
|
||||
<Ico
|
||||
icon={Icon.ThumbUp}
|
||||
className="mr-1 translate-y-[.15em] !text-base"
|
||||
/>
|
||||
{isMobile
|
||||
? prettyShortenNumber(video.likes)
|
||||
: video.likes.toLocaleString()}
|
||||
</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>
|
||||
</InsetBox>
|
||||
)}
|
||||
|
||||
<InsetBox id="description" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.description}</h2>
|
||||
<p className="whitespace-pre-line">{video.description}</p>
|
||||
</div>
|
||||
</InsetBox>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
|
||||
{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="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.description}</h2>
|
||||
<p className="whitespace-pre-line">{video.description}</p>
|
||||
</div>
|
||||
</InsetBox>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
isMobile,
|
||||
langui,
|
||||
video.channel?.data?.attributes,
|
||||
video.description,
|
||||
video.gone,
|
||||
video.likes,
|
||||
video.published_date,
|
||||
video.source,
|
||||
video.title,
|
||||
video.uid,
|
||||
video.views,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle={langui.archives}
|
||||
|
|
|
@ -5,20 +5,25 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
|||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { Icon } from "components/Ico";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {}
|
||||
|
||||
export default function Chronicles(props: Props): JSX.Element {
|
||||
const { langui } = props;
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.WatchLater}
|
||||
title={langui.chronicles}
|
||||
description={langui.chronicles_description}
|
||||
/>
|
||||
</SubPanel>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.WatchLater}
|
||||
title={langui.chronicles}
|
||||
description={langui.chronicles_description}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout navTitle={langui.chronicles} subPanel={subPanel} {...props} />
|
||||
);
|
||||
|
|
|
@ -76,321 +76,349 @@ export default function Content(props: Props): JSX.Element {
|
|||
[content.group, content.slug]
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/contents`}
|
||||
title={langui.contents}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
|
||||
{selectedTranslation?.text_set?.source_language?.data?.attributes
|
||||
?.code !== undefined && (
|
||||
<div className="grid gap-5">
|
||||
<h2 className="text-xl">
|
||||
{selectedTranslation.text_set.source_language.data.attributes
|
||||
.code === selectedTranslation.language?.data?.attributes?.code
|
||||
? langui.transcript_notice
|
||||
: langui.translation_notice}
|
||||
</h2>
|
||||
|
||||
{selectedTranslation.text_set.source_language.data.attributes.code !==
|
||||
selectedTranslation.language?.data?.attributes?.code && (
|
||||
<div className="grid place-items-center gap-2">
|
||||
<p className="font-headers">{langui.source_language}:</p>
|
||||
<Chip>
|
||||
{prettyLanguage(
|
||||
selectedTranslation.text_set.source_language.data.attributes
|
||||
.code,
|
||||
languages
|
||||
)}
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(
|
||||
selectedTranslation.text_set.status,
|
||||
langui
|
||||
)}
|
||||
maxWidth={"20rem"}
|
||||
>
|
||||
<Chip>{selectedTranslation.text_set.status}</Chip>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{selectedTranslation.text_set.transcribers &&
|
||||
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{langui.transcribers}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(
|
||||
selectedTranslation.text_set.transcribers.data
|
||||
).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={recorder.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.translators &&
|
||||
selectedTranslation.text_set.translators.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{langui.translators}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(
|
||||
selectedTranslation.text_set.translators.data
|
||||
).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={recorder.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.proofreaders &&
|
||||
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{langui.proofreaders}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(
|
||||
selectedTranslation.text_set.proofreaders.data
|
||||
).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={recorder.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
||||
<div>
|
||||
<p className="font-headers">{"Notes"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
<Markdawn text={selectedTranslation.text_set.notes} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content.ranged_contents?.data &&
|
||||
content.ranged_contents.data.length > 0 && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div>
|
||||
<p className="font-headers text-2xl">{langui.source}</p>
|
||||
<div className="mt-6 grid place-items-center gap-6 text-left">
|
||||
{content.ranged_contents.data.map((rangedContent) => {
|
||||
const libraryItem =
|
||||
rangedContent.attributes?.library_item?.data;
|
||||
if (libraryItem?.attributes && libraryItem.id) {
|
||||
return (
|
||||
<div
|
||||
key={libraryItem.attributes.slug}
|
||||
className="mobile:w-[80%]"
|
||||
>
|
||||
<PreviewCard
|
||||
href={`/library/${libraryItem.attributes.slug}`}
|
||||
title={libraryItem.attributes.title}
|
||||
subtitle={libraryItem.attributes.subtitle}
|
||||
thumbnail={
|
||||
libraryItem.attributes.thumbnail?.data?.attributes
|
||||
}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
topChips={
|
||||
libraryItem.attributes.metadata &&
|
||||
libraryItem.attributes.metadata.length > 0 &&
|
||||
libraryItem.attributes.metadata[0]
|
||||
? [
|
||||
prettyItemSubType(
|
||||
libraryItem.attributes.metadata[0]
|
||||
),
|
||||
]
|
||||
: []
|
||||
}
|
||||
bottomChips={libraryItem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
currencies: currencies,
|
||||
release_date: libraryItem.attributes.release_date,
|
||||
price: libraryItem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
<PreviewCardCTAs
|
||||
id={libraryItem.id}
|
||||
displayCTAs={
|
||||
!isUntangibleGroupItem(
|
||||
libraryItem.attributes.metadata?.[0]
|
||||
)
|
||||
}
|
||||
langui={langui}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedTranslation?.text_set?.text && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<TOC
|
||||
text={selectedTranslation.text_set.text}
|
||||
title={prettyinlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SubPanel>
|
||||
);
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href={`/contents`}
|
||||
title={langui.contents}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
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}
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/contents`}
|
||||
title={langui.contents}
|
||||
langui={langui}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
|
||||
{previousContent?.attributes && (
|
||||
<div className="mt-12 mb-8 w-full">
|
||||
<h2 className="mb-4 text-center text-2xl">
|
||||
{langui.previous_content}
|
||||
{selectedTranslation?.text_set?.source_language?.data?.attributes
|
||||
?.code !== undefined && (
|
||||
<div className="grid gap-5">
|
||||
<h2 className="text-xl">
|
||||
{selectedTranslation.text_set.source_language.data.attributes
|
||||
.code === selectedTranslation.language?.data?.attributes?.code
|
||||
? langui.transcript_notice
|
||||
: langui.translation_notice}
|
||||
</h2>
|
||||
<TranslatedPreviewLine
|
||||
href={`/contents/${previousContent.attributes.slug}`}
|
||||
translations={previousContent.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
pre_title: translation?.pre_title,
|
||||
title: translation?.title,
|
||||
subtitle: translation?.subtitle,
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
|
||||
{selectedTranslation.text_set.source_language.data.attributes
|
||||
.code !==
|
||||
selectedTranslation.language?.data?.attributes?.code && (
|
||||
<div className="grid place-items-center gap-2">
|
||||
<p className="font-headers">{langui.source_language}:</p>
|
||||
<Chip>
|
||||
{prettyLanguage(
|
||||
selectedTranslation.text_set.source_language.data.attributes
|
||||
.code,
|
||||
languages
|
||||
)}
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
||||
<p className="font-headers">{langui.status}:</p>
|
||||
|
||||
<ToolTip
|
||||
content={getStatusDescription(
|
||||
selectedTranslation.text_set.status,
|
||||
langui
|
||||
)}
|
||||
maxWidth={"20rem"}
|
||||
>
|
||||
<Chip>{selectedTranslation.text_set.status}</Chip>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{selectedTranslation.text_set.transcribers &&
|
||||
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{langui.transcribers}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(
|
||||
selectedTranslation.text_set.transcribers.data
|
||||
).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={recorder.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
slug={previousContent.attributes.slug}
|
||||
languages={languages}
|
||||
thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
topChips={
|
||||
isMobile
|
||||
? undefined
|
||||
: 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={
|
||||
isMobile
|
||||
? undefined
|
||||
: previousContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{selectedTranslation.text_set.translators &&
|
||||
selectedTranslation.text_set.translators.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{langui.translators}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(
|
||||
selectedTranslation.text_set.translators.data
|
||||
).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={recorder.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTranslation.text_set.proofreaders &&
|
||||
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers">{langui.proofreaders}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(
|
||||
selectedTranslation.text_set.proofreaders.data
|
||||
).map((recorder) => (
|
||||
<Fragment key={recorder.id}>
|
||||
<RecorderChip
|
||||
langui={langui}
|
||||
recorder={recorder.attributes}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
||||
<div>
|
||||
<p className="font-headers">{"Notes"}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
<Markdawn text={selectedTranslation.text_set.notes} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<HorizontalLine />
|
||||
{content.ranged_contents?.data &&
|
||||
content.ranged_contents.data.length > 0 && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<div>
|
||||
<p className="font-headers text-2xl">{langui.source}</p>
|
||||
<div className="mt-6 grid place-items-center gap-6 text-left">
|
||||
{content.ranged_contents.data.map((rangedContent) => {
|
||||
const libraryItem =
|
||||
rangedContent.attributes?.library_item?.data;
|
||||
if (libraryItem?.attributes && libraryItem.id) {
|
||||
return (
|
||||
<div
|
||||
key={libraryItem.attributes.slug}
|
||||
className="mobile:w-[80%]"
|
||||
>
|
||||
<PreviewCard
|
||||
href={`/library/${libraryItem.attributes.slug}`}
|
||||
title={libraryItem.attributes.title}
|
||||
subtitle={libraryItem.attributes.subtitle}
|
||||
thumbnail={
|
||||
libraryItem.attributes.thumbnail?.data?.attributes
|
||||
}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
topChips={
|
||||
libraryItem.attributes.metadata &&
|
||||
libraryItem.attributes.metadata.length > 0 &&
|
||||
libraryItem.attributes.metadata[0]
|
||||
? [
|
||||
prettyItemSubType(
|
||||
libraryItem.attributes.metadata[0]
|
||||
),
|
||||
]
|
||||
: []
|
||||
}
|
||||
bottomChips={libraryItem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
currencies: currencies,
|
||||
release_date: libraryItem.attributes.release_date,
|
||||
price: libraryItem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
<PreviewCardCTAs
|
||||
id={libraryItem.id}
|
||||
displayCTAs={
|
||||
!isUntangibleGroupItem(
|
||||
libraryItem.attributes.metadata?.[0]
|
||||
)
|
||||
}
|
||||
langui={langui}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Markdawn text={selectedTranslation?.text_set?.text ?? ""} />
|
||||
|
||||
{nextContent?.attributes && (
|
||||
{selectedTranslation?.text_set?.text && (
|
||||
<>
|
||||
<HorizontalLine />
|
||||
<h2 className="mb-4 text-center text-2xl">
|
||||
{langui.followup_content}
|
||||
</h2>
|
||||
<TranslatedPreviewLine
|
||||
href={`/contents/${nextContent.attributes.slug}`}
|
||||
translations={nextContent.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
pre_title: translation?.pre_title,
|
||||
title: translation?.title,
|
||||
subtitle: translation?.subtitle,
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
<TOC
|
||||
text={selectedTranslation.text_set.text}
|
||||
title={prettyinlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)}
|
||||
slug={nextContent.attributes.slug}
|
||||
languages={languages}
|
||||
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
topChips={
|
||||
isMobile
|
||||
? undefined
|
||||
: 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={
|
||||
isMobile
|
||||
? undefined
|
||||
: nextContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
content.ranged_contents?.data,
|
||||
currencies,
|
||||
languages,
|
||||
langui,
|
||||
selectedTranslation,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href={`/contents`}
|
||||
title={langui.contents}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
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}
|
||||
langui={langui}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
/>
|
||||
|
||||
{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={previousContent.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
pre_title: translation?.pre_title,
|
||||
title: translation?.title,
|
||||
subtitle: translation?.subtitle,
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
)}
|
||||
slug={previousContent.attributes.slug}
|
||||
languages={languages}
|
||||
thumbnail={
|
||||
previousContent.attributes.thumbnail?.data?.attributes
|
||||
}
|
||||
thumbnailAspectRatio="3/2"
|
||||
topChips={
|
||||
isMobile
|
||||
? undefined
|
||||
: 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={
|
||||
isMobile
|
||||
? undefined
|
||||
: previousContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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={nextContent.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
pre_title: translation?.pre_title,
|
||||
title: translation?.title,
|
||||
subtitle: translation?.subtitle,
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
)}
|
||||
slug={nextContent.attributes.slug}
|
||||
languages={languages}
|
||||
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
topChips={
|
||||
isMobile
|
||||
? undefined
|
||||
: 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={
|
||||
isMobile
|
||||
? undefined
|
||||
: nextContent.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
content.categories,
|
||||
content.thumbnail?.data?.attributes,
|
||||
content.type,
|
||||
isMobile,
|
||||
languages,
|
||||
langui,
|
||||
nextContent?.attributes,
|
||||
previousContent?.attributes,
|
||||
selectedTranslation,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -71,166 +71,188 @@ export default function Contents(props: Props): JSX.Element {
|
|||
[combineRelatedContent, searchName.length]
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.group_by}
|
||||
input={
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "", langui.type ?? ""]}
|
||||
state={groupingMethod}
|
||||
setState={setGroupingMethod}
|
||||
allowEmpty
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.combine_related_contents}
|
||||
disabled={searchName.length > 1}
|
||||
input={
|
||||
<Switch
|
||||
setState={setCombineRelatedContent}
|
||||
state={effectiveCombineRelatedContent}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
label={langui.group_by}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "", langui.type ?? ""]}
|
||||
state={groupingMethod}
|
||||
setState={setGroupingMethod}
|
||||
allowEmpty
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setGroupingMethod(defaultFiltersState.groupingMethod);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
setCombineRelatedContent(defaultFiltersState.combineRelatedContent);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
);
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{/* TODO: Add to langui */}
|
||||
{groups.size === 0 && (
|
||||
<ContentPlaceholder
|
||||
message={
|
||||
"No results. You can try changing or resetting the search parameters."
|
||||
<WithLabel
|
||||
label={langui.combine_related_contents}
|
||||
disabled={searchName.length > 1}
|
||||
input={
|
||||
<Switch
|
||||
setState={setCombineRelatedContent}
|
||||
state={effectiveCombineRelatedContent}
|
||||
/>
|
||||
}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
)}
|
||||
{iterateMap(
|
||||
groups,
|
||||
(name, items, index) =>
|
||||
items.length > 0 && (
|
||||
<Fragment key={index}>
|
||||
{name && (
|
||||
<h2
|
||||
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setGroupingMethod(defaultFiltersState.groupingMethod);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
setCombineRelatedContent(defaultFiltersState.combineRelatedContent);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
effectiveCombineRelatedContent,
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
searchName,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{/* TODO: Add to langui */}
|
||||
{groups.size === 0 && (
|
||||
<ContentPlaceholder
|
||||
message={
|
||||
"No results. You can try changing or resetting the search parameters."
|
||||
}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
)}
|
||||
{iterateMap(
|
||||
groups,
|
||||
(name, items, index) =>
|
||||
items.length > 0 && (
|
||||
<Fragment key={index}>
|
||||
{name && (
|
||||
<h2
|
||||
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
|
||||
first-of-type:pt-0"
|
||||
>
|
||||
{name}
|
||||
<Chip>{`${items.reduce((currentSum, item) => {
|
||||
if (effectiveCombineRelatedContent) {
|
||||
if (
|
||||
item.attributes?.group?.data?.attributes?.combine ===
|
||||
true
|
||||
) {
|
||||
return (
|
||||
currentSum +
|
||||
(item.attributes.group.data.attributes.contents?.data
|
||||
.length ?? 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
return currentSum + 1;
|
||||
}, 0)} ${
|
||||
items.length <= 1
|
||||
? langui.result?.toLowerCase() ?? ""
|
||||
: langui.results?.toLowerCase() ?? ""
|
||||
}`}</Chip>
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid grid-cols-2 items-end gap-8
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4"
|
||||
>
|
||||
{filterHasAttributes(items).map((item) => (
|
||||
<Fragment key={item.id}>
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={item.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
pre_title: translation?.pre_title,
|
||||
title: translation?.title,
|
||||
subtitle: translation?.subtitle,
|
||||
language:
|
||||
translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
)}
|
||||
slug={item.attributes.slug}
|
||||
languages={languages}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
stackNumber={
|
||||
effectiveCombineRelatedContent &&
|
||||
item.attributes.group?.data?.attributes?.combine ===
|
||||
>
|
||||
{name}
|
||||
<Chip>{`${items.reduce((currentSum, item) => {
|
||||
if (effectiveCombineRelatedContent) {
|
||||
if (
|
||||
item.attributes?.group?.data?.attributes?.combine ===
|
||||
true
|
||||
? item.attributes.group.data.attributes.contents?.data
|
||||
.length
|
||||
: 0
|
||||
) {
|
||||
return (
|
||||
currentSum +
|
||||
(item.attributes.group.data.attributes.contents
|
||||
?.data.length ?? 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]
|
||||
?.title
|
||||
: prettySlug(
|
||||
item.attributes.type.data.attributes.slug
|
||||
),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</ContentPanel>
|
||||
return currentSum + 1;
|
||||
}, 0)} ${
|
||||
items.length <= 1
|
||||
? langui.result?.toLowerCase() ?? ""
|
||||
: langui.results?.toLowerCase() ?? ""
|
||||
}`}</Chip>
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid grid-cols-2 items-end gap-8
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4"
|
||||
>
|
||||
{filterHasAttributes(items).map((item) => (
|
||||
<Fragment key={item.id}>
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={item.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
pre_title: translation?.pre_title,
|
||||
title: translation?.title,
|
||||
subtitle: translation?.subtitle,
|
||||
language:
|
||||
translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
)}
|
||||
slug={item.attributes.slug}
|
||||
languages={languages}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
stackNumber={
|
||||
effectiveCombineRelatedContent &&
|
||||
item.attributes.group?.data?.attributes?.combine ===
|
||||
true
|
||||
? item.attributes.group.data.attributes.contents
|
||||
?.data.length
|
||||
: 0
|
||||
}
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes
|
||||
.titles[0]?.title
|
||||
: prettySlug(
|
||||
item.attributes.type.data.attributes.slug
|
||||
),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
effectiveCombineRelatedContent,
|
||||
groups,
|
||||
keepInfoVisible,
|
||||
languages,
|
||||
langui.result,
|
||||
langui.results,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle={langui.contents}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getReadySdk } from "graphql/sdk";
|
|||
import { filterDefined, filterHasAttributes } from "helpers/others";
|
||||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
contents: DevGetContentsQuery;
|
||||
|
@ -21,61 +22,65 @@ export default function CheckupContents(props: Props): JSX.Element {
|
|||
const { contents } = props;
|
||||
const testReport = testingContent(contents);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
|
||||
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p className="font-headers">Ref</p>
|
||||
<p className="font-headers">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
|
||||
{testReport.lines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
|
||||
justify-items-start gap-2"
|
||||
>
|
||||
<Button
|
||||
href={line.frontendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="F"
|
||||
/>
|
||||
<Button
|
||||
href={line.backendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="B"
|
||||
/>
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip>{line.type}</Chip>
|
||||
<Chip
|
||||
className={
|
||||
line.severity === "Very High"
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === "High"
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === "Medium"
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{line.severity}
|
||||
</Chip>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
<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">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
|
||||
{testReport.lines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
|
||||
justify-items-start gap-2"
|
||||
>
|
||||
<Button
|
||||
href={line.frontendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="F"
|
||||
/>
|
||||
<Button
|
||||
href={line.backendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="B"
|
||||
/>
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip>{line.type}</Chip>
|
||||
<Chip
|
||||
className={
|
||||
line.severity === "Very High"
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === "High"
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === "Medium"
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{line.severity}
|
||||
</Chip>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[testReport.lines, testReport.title]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} />
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
|||
import { getReadySdk } from "graphql/sdk";
|
||||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
libraryItems: DevGetLibraryItemsQuery;
|
||||
|
@ -23,61 +24,65 @@ export default function CheckupLibraryItems(props: Props): JSX.Element {
|
|||
const { libraryItems } = props;
|
||||
const testReport = testingLibraryItem(libraryItems);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{<h2 className="text-2xl">{testReport.title}</h2>}
|
||||
|
||||
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p className="font-headers">Ref</p>
|
||||
<p className="font-headers">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
|
||||
{testReport.lines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid
|
||||
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2"
|
||||
>
|
||||
<Button
|
||||
href={line.frontendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="F"
|
||||
/>
|
||||
<Button
|
||||
href={line.backendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="B"
|
||||
/>
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip>{line.type}</Chip>
|
||||
<Chip
|
||||
className={
|
||||
line.severity === "Very High"
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === "High"
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === "Medium"
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{line.severity}
|
||||
</Chip>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
<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">Name</p>
|
||||
<p className="font-headers">Type</p>
|
||||
<p className="font-headers">Severity</p>
|
||||
<p className="font-headers">Description</p>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
|
||||
{testReport.lines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 grid
|
||||
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2"
|
||||
>
|
||||
<Button
|
||||
href={line.frontendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="F"
|
||||
/>
|
||||
<Button
|
||||
href={line.backendUrl}
|
||||
target="_blank"
|
||||
className="w-4 text-xs"
|
||||
text="B"
|
||||
/>
|
||||
<p>{line.subitems.join(" -> ")}</p>
|
||||
<p>{line.name}</p>
|
||||
<Chip>{line.type}</Chip>
|
||||
<Chip
|
||||
className={
|
||||
line.severity === "Very High"
|
||||
? "bg-[#f00] font-bold !opacity-100"
|
||||
: line.severity === "High"
|
||||
? "bg-[#ff6600] font-bold !opacity-100"
|
||||
: line.severity === "Medium"
|
||||
? "bg-[#fff344] !opacity-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{line.severity}
|
||||
</Chip>
|
||||
<ToolTip content={line.recommandation} placement="left">
|
||||
<p>{line.description}</p>
|
||||
</ToolTip>
|
||||
</div>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[testReport.lines, testReport.title]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} />
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { ToolTip } from "components/ToolTip";
|
|||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import TurndownService from "turndown";
|
||||
import { Icon } from "components/Ico";
|
||||
import { TOC } from "components/Markdown/TOC";
|
||||
|
@ -25,408 +25,445 @@ export default function Editor(props: Props): JSX.Element {
|
|||
const [markdown, setMarkdown] = useState("");
|
||||
const [converterOpened, setConverterOpened] = useState(false);
|
||||
|
||||
function wrap(
|
||||
wrapper: string,
|
||||
properties?: Record<string, string>,
|
||||
addInnerNewLines?: boolean
|
||||
) {
|
||||
transformationWrapper((value, selectionStart, selectionEnd) => {
|
||||
let prepend = wrapper;
|
||||
let append = wrapper;
|
||||
const transformationWrapper = useCallback(
|
||||
(
|
||||
transformation: (
|
||||
value: string,
|
||||
selectionStart: number,
|
||||
selectedEnd: number
|
||||
) => { prependLength: number; transformedValue: string }
|
||||
) => {
|
||||
const textarea =
|
||||
document.querySelector<HTMLTextAreaElement>("#editorTextArea");
|
||||
if (textarea) {
|
||||
const { value, selectionStart, selectionEnd } = textarea;
|
||||
|
||||
if (properties) {
|
||||
prepend = `<${wrapper}${Object.entries(properties).map(
|
||||
([propertyName, propertyValue]) =>
|
||||
` ${propertyName}="${propertyValue}"`
|
||||
)}>`;
|
||||
append = `</${wrapper}>`;
|
||||
const { prependLength, transformedValue } = transformation(
|
||||
value,
|
||||
selectionStart,
|
||||
selectionEnd
|
||||
);
|
||||
|
||||
textarea.value = transformedValue;
|
||||
handleInput(textarea.value);
|
||||
|
||||
textarea.focus();
|
||||
textarea.selectionStart = selectionStart + prependLength;
|
||||
textarea.selectionEnd = selectionEnd + prependLength;
|
||||
}
|
||||
},
|
||||
[handleInput]
|
||||
);
|
||||
|
||||
if (addInnerNewLines === true) {
|
||||
prepend = `${prepend}\n`;
|
||||
append = `\n${append}`;
|
||||
const wrap = useCallback(
|
||||
(
|
||||
wrapper: string,
|
||||
properties?: Record<string, string>,
|
||||
addInnerNewLines?: boolean
|
||||
) => {
|
||||
transformationWrapper((value, selectionStart, selectionEnd) => {
|
||||
let prepend = wrapper;
|
||||
let append = wrapper;
|
||||
|
||||
if (properties) {
|
||||
prepend = `<${wrapper}${Object.entries(properties).map(
|
||||
([propertyName, propertyValue]) =>
|
||||
` ${propertyName}="${propertyValue}"`
|
||||
)}>`;
|
||||
append = `</${wrapper}>`;
|
||||
}
|
||||
|
||||
if (addInnerNewLines === true) {
|
||||
prepend = `${prepend}\n`;
|
||||
append = `\n${append}`;
|
||||
}
|
||||
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, selectionStart);
|
||||
newValue += prepend;
|
||||
newValue += value.slice(selectionStart, selectionEnd);
|
||||
newValue += append;
|
||||
newValue += value.slice(selectionEnd);
|
||||
return { prependLength: prepend.length, transformedValue: newValue };
|
||||
});
|
||||
},
|
||||
[transformationWrapper]
|
||||
);
|
||||
|
||||
const unwrap = useCallback(
|
||||
(wrapper: string) => {
|
||||
transformationWrapper((value, selectionStart, selectionEnd) => {
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, selectionStart - wrapper.length);
|
||||
newValue += value.slice(selectionStart, selectionEnd);
|
||||
newValue += value.slice(wrapper.length + selectionEnd);
|
||||
return { prependLength: -wrapper.length, transformedValue: newValue };
|
||||
});
|
||||
},
|
||||
[transformationWrapper]
|
||||
);
|
||||
|
||||
const toggleWrap = useCallback(
|
||||
(
|
||||
wrapper: string,
|
||||
properties?: Record<string, string>,
|
||||
addInnerNewLines?: boolean
|
||||
) => {
|
||||
const textarea =
|
||||
document.querySelector<HTMLTextAreaElement>("#editorTextArea");
|
||||
if (textarea) {
|
||||
const { value, selectionStart, selectionEnd } = textarea;
|
||||
|
||||
if (
|
||||
value.slice(selectionStart - wrapper.length, selectionStart) ===
|
||||
wrapper &&
|
||||
value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper
|
||||
) {
|
||||
unwrap(wrapper);
|
||||
} else {
|
||||
wrap(wrapper, properties, addInnerNewLines);
|
||||
}
|
||||
}
|
||||
},
|
||||
[unwrap, wrap]
|
||||
);
|
||||
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, selectionStart);
|
||||
newValue += prepend;
|
||||
newValue += value.slice(selectionStart, selectionEnd);
|
||||
newValue += append;
|
||||
newValue += value.slice(selectionEnd);
|
||||
return { prependLength: prepend.length, transformedValue: newValue };
|
||||
});
|
||||
}
|
||||
const preline = useCallback(
|
||||
(prepend: string) => {
|
||||
transformationWrapper((value, selectionStart) => {
|
||||
const lastNewLine =
|
||||
value.slice(0, selectionStart).lastIndexOf("\n") + 1;
|
||||
|
||||
function toggleWrap(
|
||||
wrapper: string,
|
||||
properties?: Record<string, string>,
|
||||
addInnerNewLines?: boolean
|
||||
) {
|
||||
const textarea =
|
||||
document.querySelector<HTMLTextAreaElement>("#editorTextArea");
|
||||
if (textarea) {
|
||||
const { value, selectionStart, selectionEnd } = textarea;
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, lastNewLine);
|
||||
newValue += prepend;
|
||||
newValue += value.slice(lastNewLine);
|
||||
|
||||
if (
|
||||
value.slice(selectionStart - wrapper.length, selectionStart) ===
|
||||
wrapper &&
|
||||
value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper
|
||||
) {
|
||||
unwrap(wrapper);
|
||||
} else {
|
||||
wrap(wrapper, properties, addInnerNewLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { prependLength: prepend.length, transformedValue: newValue };
|
||||
});
|
||||
},
|
||||
[transformationWrapper]
|
||||
);
|
||||
|
||||
function unwrap(wrapper: string) {
|
||||
transformationWrapper((value, selectionStart, selectionEnd) => {
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, selectionStart - wrapper.length);
|
||||
newValue += value.slice(selectionStart, selectionEnd);
|
||||
newValue += value.slice(wrapper.length + selectionEnd);
|
||||
return { prependLength: -wrapper.length, transformedValue: newValue };
|
||||
});
|
||||
}
|
||||
const insert = useCallback(
|
||||
(prepend: string) => {
|
||||
transformationWrapper((value, selectionStart) => {
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, selectionStart);
|
||||
newValue += prepend;
|
||||
newValue += value.slice(selectionStart);
|
||||
|
||||
function preline(prepend: string) {
|
||||
transformationWrapper((value, selectionStart) => {
|
||||
const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1;
|
||||
return { prependLength: prepend.length, transformedValue: newValue };
|
||||
});
|
||||
},
|
||||
[transformationWrapper]
|
||||
);
|
||||
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, lastNewLine);
|
||||
newValue += prepend;
|
||||
newValue += value.slice(lastNewLine);
|
||||
const appendDoc = useCallback(
|
||||
(append: string) => {
|
||||
transformationWrapper((value) => {
|
||||
const newValue = value + append;
|
||||
return { prependLength: 0, transformedValue: newValue };
|
||||
});
|
||||
},
|
||||
[transformationWrapper]
|
||||
);
|
||||
|
||||
return { prependLength: prepend.length, transformedValue: newValue };
|
||||
});
|
||||
}
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<Popup setState={setConverterOpened} state={converterOpened}>
|
||||
<div className="text-center">
|
||||
<h2 className="mt-4">Convert HTML to markdown</h2>
|
||||
<p>
|
||||
Copy and paste any HTML content (content from web pages) here.
|
||||
<br />
|
||||
The text will immediatly be converted to valid Markdown.
|
||||
<br />
|
||||
You can then copy the converted text and paste it anywhere you
|
||||
want in the editor
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
id="htmlMdTextArea"
|
||||
title="Ouput textarea"
|
||||
onPaste={(event) => {
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
bulletListMarker: "-",
|
||||
emDelimiter: "_",
|
||||
strongDelimiter: "**",
|
||||
});
|
||||
|
||||
function insert(prepend: string) {
|
||||
transformationWrapper((value, selectionStart) => {
|
||||
let newValue = "";
|
||||
newValue += value.slice(0, selectionStart);
|
||||
newValue += prepend;
|
||||
newValue += value.slice(selectionStart);
|
||||
let paste = event.clipboardData.getData("text/html");
|
||||
paste = paste.replace(/<!--.*?-->/u, "");
|
||||
paste = turndownService.turndown(paste);
|
||||
paste = paste.replace(/<!--.*?-->/u, "");
|
||||
|
||||
return { prependLength: prepend.length, transformedValue: newValue };
|
||||
});
|
||||
}
|
||||
|
||||
function appendDoc(append: string) {
|
||||
transformationWrapper((value) => {
|
||||
const newValue = value + append;
|
||||
return { prependLength: 0, transformedValue: newValue };
|
||||
});
|
||||
}
|
||||
|
||||
function transformationWrapper(
|
||||
transformation: (
|
||||
value: string,
|
||||
selectionStart: number,
|
||||
selectedEnd: number
|
||||
) => { prependLength: number; transformedValue: string }
|
||||
) {
|
||||
const textarea =
|
||||
document.querySelector<HTMLTextAreaElement>("#editorTextArea");
|
||||
if (textarea) {
|
||||
const { value, selectionStart, selectionEnd } = textarea;
|
||||
|
||||
const { prependLength, transformedValue } = transformation(
|
||||
value,
|
||||
selectionStart,
|
||||
selectionEnd
|
||||
);
|
||||
|
||||
textarea.value = transformedValue;
|
||||
handleInput(textarea.value);
|
||||
|
||||
textarea.focus();
|
||||
textarea.selectionStart = selectionStart + prependLength;
|
||||
textarea.selectionEnd = selectionEnd + prependLength;
|
||||
}
|
||||
}
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<Popup setState={setConverterOpened} state={converterOpened}>
|
||||
<div className="text-center">
|
||||
<h2 className="mt-4">Convert HTML to markdown</h2>
|
||||
<p>
|
||||
Copy and paste any HTML content (content from web pages) here.{" "}
|
||||
<br />
|
||||
The text will immediatly be converted to valid Markdown.
|
||||
<br />
|
||||
You can then copy the converted text and paste it anywhere you want
|
||||
in the editor
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
id="htmlMdTextArea"
|
||||
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] mobile:w-[75vw]"
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<div className="mb-4 flex flex-row gap-2">
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<h3 className="text-lg">Headers</h3>
|
||||
<Button onClick={() => preline("# ")} text={"H1"} />
|
||||
<Button onClick={() => preline("## ")} text={"H2"} />
|
||||
<Button onClick={() => preline("### ")} text={"H3"} />
|
||||
<Button onClick={() => preline("#### ")} text={"H4"} />
|
||||
<Button onClick={() => preline("##### ")} text={"H5"} />
|
||||
<Button onClick={() => preline("###### ")} text={"H6"} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button icon={Icon.Title} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Toggle Bold</h3>}
|
||||
>
|
||||
<Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Toggle Italic</h3>}
|
||||
>
|
||||
<Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Toggle Inline Code</h3>
|
||||
<p>
|
||||
Makes the text monospace (like text from a computer terminal).
|
||||
Usually used for stylistic purposes in transcripts.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Insert footnote</h3>
|
||||
<p>When inserted “x”</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
insert("[^x]");
|
||||
appendDoc("\n\n[^x]: This is a footnote.");
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
target.value = paste;
|
||||
target.select();
|
||||
event.preventDefault();
|
||||
}}
|
||||
icon={Icon.Superscript}
|
||||
className="h-[50vh] w-[50vw] mobile:w-[75vw]"
|
||||
/>
|
||||
</ToolTip>
|
||||
</Popup>
|
||||
|
||||
<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="mb-4 flex flex-row gap-2">
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="grid gap-2">
|
||||
<h3 className="text-lg">Headers</h3>
|
||||
<Button onClick={() => preline("# ")} text={"H1"} />
|
||||
<Button onClick={() => preline("## ")} text={"H2"} />
|
||||
<Button onClick={() => preline("### ")} text={"H3"} />
|
||||
<Button onClick={() => preline("#### ")} text={"H4"} />
|
||||
<Button onClick={() => preline("##### ")} text={"H5"} />
|
||||
<Button onClick={() => preline("###### ")} text={"H6"} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button icon={Icon.Title} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Toggle Bold</h3>}
|
||||
>
|
||||
<Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Toggle Italic</h3>}
|
||||
>
|
||||
<Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Toggle Inline Code</h3>
|
||||
<p>
|
||||
Makes the text monospace (like text from a computer terminal).
|
||||
Usually used for stylistic purposes in transcripts.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Insert footnote</h3>
|
||||
<p>When inserted “x”</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
insert("[^x]");
|
||||
appendDoc("\n\n[^x]: This is a footnote.");
|
||||
}}
|
||||
icon={Icon.Superscript}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcripts</h3>
|
||||
<p>
|
||||
Use this to create dialogues and transcripts. Start by adding
|
||||
a container, then add transcript speech line within.
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcript container</h3>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("Transcript", {}, true)}
|
||||
icon={Icon.AddBox}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Transcript speech line</h3>
|
||||
<p>
|
||||
Use to add a dialogue/transcript line. Change the{" "}
|
||||
<kbd>name</kbd> property to chang the name of the
|
||||
speaker
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("Line", { name: "speaker" })}
|
||||
icon={Icon.RecordVoiceOver}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button icon={Icon.RecordVoiceOver} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Inset box</h3>}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("InsetBox", {}, true)}
|
||||
icon={Icon.CheckBoxOutlineBlank}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Scene break</h3>}
|
||||
>
|
||||
<Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} />
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={
|
||||
<div className="flex flex-col place-items-center gap-2">
|
||||
<h3 className="text-lg">Links</h3>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">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
|
||||
<h3 className="text-lg">External Link</h3>
|
||||
<p className="text-xs">
|
||||
Provides a link to another webpage / website
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("Line", { name: "speaker" })}
|
||||
icon={Icon.RecordVoiceOver}
|
||||
onClick={() => insert("[Link name](https://domain.com)")}
|
||||
icon={Icon.Link}
|
||||
text={"External"}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink</h3>
|
||||
<p className="text-xs">
|
||||
Interlinks are used to add links to a header within the
|
||||
same document
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", {})}
|
||||
icon={Icon.Link}
|
||||
text={"Internal"}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink (with target)</h3>{" "}
|
||||
<p className="text-xs">
|
||||
Use this one if you want the intralink text to be
|
||||
different from the target header’s name.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", { target: "target" })}
|
||||
icon={Icon.Link}
|
||||
text="Internal (w/ target)"
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button icon={Icon.RecordVoiceOver} />
|
||||
</ToolTip>
|
||||
}
|
||||
>
|
||||
<Button icon={Icon.Link} />
|
||||
</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="bottom"
|
||||
content={
|
||||
<h3 className="text-lg">Player’s name placeholder</h3>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => insert("<player>")} icon={Icon.Person} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink</h3>
|
||||
<p className="text-xs">
|
||||
Interlinks are used to add links to a header within the
|
||||
same document
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", {})}
|
||||
icon={Icon.Link}
|
||||
text={"Internal"}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
placement="right"
|
||||
content={
|
||||
<>
|
||||
<h3 className="text-lg">Intralink (with target)</h3>{" "}
|
||||
<p className="text-xs">
|
||||
Use this one if you want the intralink text to be
|
||||
different from the target header’s name.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => wrap("IntraLink", { target: "target" })}
|
||||
icon={Icon.Link}
|
||||
text="Internal (w/ target)"
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button icon={Icon.Link} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Player’s name placeholder</h3>}
|
||||
>
|
||||
<Button onClick={() => insert("<player>")} icon={Icon.Person} />
|
||||
</ToolTip>
|
||||
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Open HTML Converter</h3>}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setConverterOpened(true);
|
||||
}}
|
||||
icon={Icon.Html}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h2>Editor</h2>
|
||||
<textarea
|
||||
id="editorTextArea"
|
||||
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"
|
||||
/>
|
||||
<ToolTip
|
||||
placement="bottom"
|
||||
content={<h3 className="text-lg">Open HTML Converter</h3>}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setConverterOpened(true);
|
||||
}}
|
||||
icon={Icon.Html}
|
||||
/>
|
||||
</ToolTip>
|
||||
</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 className="grid grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h2>Editor</h2>
|
||||
<textarea
|
||||
id="editorTextArea"
|
||||
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 className="mt-8">
|
||||
<TOC text={markdown} />
|
||||
</div>
|
||||
</ContentPanel>
|
||||
<div className="mt-8">
|
||||
<TOC text={markdown} />
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
appendDoc,
|
||||
converterOpened,
|
||||
handleInput,
|
||||
insert,
|
||||
markdown,
|
||||
preline,
|
||||
toggleWrap,
|
||||
wrap,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle="Markdawn Editor"
|
||||
|
|
|
@ -49,15 +49,17 @@ import {
|
|||
GetStaticPathsResult,
|
||||
GetStaticPropsContext,
|
||||
} from "next";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
||||
import { useMediaHoverable } from "hooks/useMediaQuery";
|
||||
import { WithLabel } from "components/Inputs/WithLabel";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
item: NonNullable<
|
||||
GetLibraryItemQuery["libraryItems"]
|
||||
>["data"][number]["attributes"];
|
||||
NonNullable<
|
||||
GetLibraryItemQuery["libraryItems"]
|
||||
>["data"][number]["attributes"]
|
||||
>;
|
||||
itemId: NonNullable<
|
||||
GetLibraryItemQuery["libraryItems"]
|
||||
>["data"][number]["id"];
|
||||
|
@ -67,448 +69,474 @@ export default function LibrarySlug(props: Props): JSX.Element {
|
|||
const { item, itemId, langui, currencies } = props;
|
||||
const appLayout = useAppLayout();
|
||||
const hoverable = useMediaHoverable();
|
||||
|
||||
useScrollTopOnChange(AnchorIds.ContentPanel, [item]);
|
||||
|
||||
const isVariantSet =
|
||||
item?.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
|
||||
|
||||
sortContent(item?.contents);
|
||||
|
||||
const [openLightBox, LightBox] = useLightBox();
|
||||
const [keepInfoVisible, setKeepInfoVisible] = useState(false);
|
||||
|
||||
let displayOpenScans = false;
|
||||
if (item?.contents?.data)
|
||||
for (const content of item.contents.data) {
|
||||
if (
|
||||
content.attributes?.scan_set &&
|
||||
content.attributes.scan_set.length > 0
|
||||
)
|
||||
displayOpenScans = true;
|
||||
}
|
||||
useScrollTopOnChange(AnchorIds.ContentPanel, [item]);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<NavOption title={langui.summary} url="#summary" border />
|
||||
|
||||
{item?.gallery && item.gallery.data.length > 0 && (
|
||||
<NavOption title={langui.gallery} url="#gallery" border />
|
||||
)}
|
||||
|
||||
<NavOption title={langui.details} url="#details" border />
|
||||
|
||||
{item?.subitems && item.subitems.data.length > 0 && (
|
||||
<NavOption
|
||||
title={isVariantSet ? langui.variants : langui.subitems}
|
||||
url={isVariantSet ? "#variants" : "#subitems"}
|
||||
border
|
||||
/>
|
||||
)}
|
||||
|
||||
{item?.contents && item.contents.data.length > 0 && (
|
||||
<NavOption title={langui.contents} url="#contents" border />
|
||||
)}
|
||||
</div>
|
||||
</SubPanel>
|
||||
const isVariantSet = useMemo(
|
||||
() =>
|
||||
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set",
|
||||
[item.metadata]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<LightBox />
|
||||
const displayOpenScans = useMemo(
|
||||
() =>
|
||||
item.contents?.data.some(
|
||||
(content) =>
|
||||
content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||
),
|
||||
[item.contents?.data]
|
||||
);
|
||||
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
className="relative h-[50vh] w-full
|
||||
cursor-pointer drop-shadow-shade-xl desktop:mb-16 mobile:h-[60vh]"
|
||||
onClick={() => {
|
||||
if (item?.thumbnail?.data?.attributes) {
|
||||
openLightBox([
|
||||
getAssetURL(
|
||||
item.thumbnail.data.attributes.url,
|
||||
ImageQuality.Large
|
||||
),
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item?.thumbnail?.data?.attributes ? (
|
||||
<Img
|
||||
image={item.thumbnail.data.attributes}
|
||||
quality={ImageQuality.Large}
|
||||
className="h-full w-full object-contain"
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<NavOption title={langui.summary} url="#summary" border />
|
||||
|
||||
{item.gallery && item.gallery.data.length > 0 && (
|
||||
<NavOption title={langui.gallery} url="#gallery" border />
|
||||
)}
|
||||
|
||||
<NavOption title={langui.details} url="#details" border />
|
||||
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<NavOption
|
||||
title={isVariantSet ? langui.variants : langui.subitems}
|
||||
url={isVariantSet ? "#variants" : "#subitems"}
|
||||
border
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-[21/29.7] w-full rounded-xl bg-light"></div>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<NavOption title={langui.contents} url="#contents" border />
|
||||
)}
|
||||
</div>
|
||||
</SubPanel>
|
||||
),
|
||||
[isVariantSet, item.contents, item.gallery, item.subitems, langui]
|
||||
);
|
||||
|
||||
<InsetBox id="summary" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
{item?.subitem_of?.data[0]?.attributes && (
|
||||
<div className="grid place-items-center">
|
||||
<p>{langui.subitem_of}</p>
|
||||
<Button
|
||||
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
|
||||
text={prettyinlineTitle(
|
||||
"",
|
||||
item.subitem_of.data[0].attributes.title,
|
||||
item.subitem_of.data[0].attributes.subtitle
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<LightBox />
|
||||
|
||||
<ReturnButton
|
||||
href="/library/"
|
||||
title={langui.library}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
<div className="grid place-items-center gap-12">
|
||||
<div
|
||||
className="relative h-[50vh] w-full
|
||||
cursor-pointer drop-shadow-shade-xl desktop:mb-16 mobile:h-[60vh]"
|
||||
onClick={() => {
|
||||
if (item.thumbnail?.data?.attributes) {
|
||||
openLightBox([
|
||||
getAssetURL(
|
||||
item.thumbnail.data.attributes.url,
|
||||
ImageQuality.Large
|
||||
),
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.thumbnail?.data?.attributes ? (
|
||||
<Img
|
||||
image={item.thumbnail.data.attributes}
|
||||
quality={ImageQuality.Large}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-[21/29.7] w-full rounded-xl bg-light"></div>
|
||||
)}
|
||||
<div className="grid place-items-center text-center">
|
||||
<h1 className="text-3xl">{item?.title}</h1>
|
||||
{item && isDefinedAndNotEmpty(item.subtitle) && (
|
||||
<h2 className="text-2xl">{item.subtitle}</h2>
|
||||
</div>
|
||||
|
||||
<InsetBox id="summary" className="grid place-items-center">
|
||||
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
|
||||
{item.subitem_of?.data[0]?.attributes && (
|
||||
<div className="grid place-items-center">
|
||||
<p>{langui.subitem_of}</p>
|
||||
<Button
|
||||
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
|
||||
text={prettyinlineTitle(
|
||||
"",
|
||||
item.subitem_of.data[0].attributes.title,
|
||||
item.subitem_of.data[0].attributes.subtitle
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PreviewCardCTAs
|
||||
id={itemId}
|
||||
displayCTAs={!isUntangibleGroupItem(item?.metadata?.[0])}
|
||||
langui={langui}
|
||||
expand
|
||||
/>
|
||||
{item?.descriptions?.[0] && (
|
||||
<p className="text-justify">{item.descriptions[0].description}</p>
|
||||
)}
|
||||
{!(
|
||||
item?.metadata &&
|
||||
item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
(item.metadata[0].subtype?.data?.attributes?.slug ===
|
||||
"variant-set" ||
|
||||
item.metadata[0].subtype?.data?.attributes?.slug ===
|
||||
"relation-set")
|
||||
) && (
|
||||
<>
|
||||
{item?.urls && item.urls.length ? (
|
||||
<div className="flex flex-row place-items-center gap-3">
|
||||
<p>{langui.available_at}</p>
|
||||
{filterHasAttributes(item.urls).map((url, index) => (
|
||||
<Fragment key={index}>
|
||||
<Button
|
||||
href={url.url}
|
||||
target={"_blank"}
|
||||
text={prettyURL(url.url)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{langui.item_not_available}</p>
|
||||
<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>
|
||||
</InsetBox>
|
||||
|
||||
{item?.gallery && item.gallery.data.length > 0 && (
|
||||
<div id="gallery" className="grid w-full place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.gallery}</h2>
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
|
||||
gap-8"
|
||||
>
|
||||
{filterHasAttributes(item.gallery.data).map(
|
||||
(galleryItem, index) => (
|
||||
<Fragment key={galleryItem.id}>
|
||||
<div
|
||||
className="relative aspect-square cursor-pointer
|
||||
transition-transform hover:scale-[1.02]"
|
||||
onClick={() => {
|
||||
const images: string[] = filterHasAttributes(
|
||||
item.gallery?.data
|
||||
).map((image) =>
|
||||
getAssetURL(image.attributes.url, ImageQuality.Large)
|
||||
);
|
||||
openLightBox(images, index);
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
className="h-full w-full rounded-lg
|
||||
bg-light object-cover drop-shadow-shade-md"
|
||||
image={galleryItem.attributes}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<InsetBox id="details" className="grid place-items-center">
|
||||
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-8">
|
||||
<h2 className="text-center text-2xl">{langui.details}</h2>
|
||||
<div
|
||||
className="grid place-items-center gap-y-8
|
||||
desktop:grid-flow-col desktop: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>{prettyItemType(item.metadata[0], langui)}</Chip>
|
||||
{"›"}
|
||||
<Chip>{prettyItemSubType(item.metadata[0])}</Chip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item?.release_date && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{langui.release_date}</h3>
|
||||
<p>{prettyDate(item.release_date)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item?.price && (
|
||||
<div className="grid place-content-start place-items-center text-center">
|
||||
<h3 className="text-xl">{langui.price}</h3>
|
||||
<p>
|
||||
{prettyPrice(
|
||||
item.price,
|
||||
currencies,
|
||||
item.price.currency?.data?.attributes?.code
|
||||
)}
|
||||
</p>
|
||||
{item.price.currency?.data?.attributes?.code !==
|
||||
appLayout.currency && (
|
||||
<p>
|
||||
{prettyPrice(item.price, currencies, appLayout.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">
|
||||
{item.categories.data.map((category) => (
|
||||
<Chip key={category.id}>{category.attributes?.name}</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item?.size && (
|
||||
<div className="grid gap-8 mobile:place-items-center">
|
||||
<h3 className="text-xl">{langui.size}</h3>
|
||||
<div
|
||||
className="grid w-full grid-flow-col place-content-between thin:grid-flow-row
|
||||
thin:place-content-center thin:gap-8"
|
||||
>
|
||||
<div
|
||||
className="grid place-items-center gap-x-4 desktop:grid-flow-col
|
||||
desktop:place-items-start"
|
||||
>
|
||||
<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="grid place-items-center gap-x-4 desktop:grid-flow-col
|
||||
desktop:place-items-start"
|
||||
>
|
||||
<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="grid place-items-center gap-x-4 desktop:grid-flow-col
|
||||
desktop:place-items-start"
|
||||
>
|
||||
<p className="font-bold">{langui.thickness}:</p>
|
||||
<div>
|
||||
<p>{item.size.thickness} mm</p>
|
||||
<p>{convertMmToInch(item.size.thickness)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item?.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
|
||||
item?.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
|
||||
<PreviewCardCTAs
|
||||
id={itemId}
|
||||
displayCTAs={!isUntangibleGroupItem(item.metadata?.[0])}
|
||||
langui={langui}
|
||||
expand
|
||||
/>
|
||||
{item.descriptions?.[0] && (
|
||||
<p className="text-justify">
|
||||
{item.descriptions[0].description}
|
||||
</p>
|
||||
)}
|
||||
{!(
|
||||
item.metadata &&
|
||||
item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
(item.metadata[0].subtype?.data?.attributes?.slug ===
|
||||
"variant-set" ||
|
||||
item.metadata[0].subtype?.data?.attributes?.slug ===
|
||||
"relation-set")
|
||||
) && (
|
||||
<>
|
||||
<h3 className="text-xl">{langui.type_information}</h3>
|
||||
<div className="grid w-full grid-cols-2 place-content-between">
|
||||
{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">
|
||||
<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>
|
||||
{item.urls?.length ? (
|
||||
<div className="flex flex-row place-items-center gap-3">
|
||||
<p>{langui.available_at}</p>
|
||||
{filterHasAttributes(item.urls).map((url, index) => (
|
||||
<Fragment key={index}>
|
||||
<Button
|
||||
href={url.url}
|
||||
target={"_blank"}
|
||||
text={prettyURL(url.url)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{langui.item_not_available}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</InsetBox>
|
||||
</div>
|
||||
</InsetBox>
|
||||
|
||||
{item?.subitems && item.subitems.data.length > 0 && (
|
||||
<div
|
||||
id={isVariantSet ? "variants" : "subitems"}
|
||||
className="grid w-full place-items-center gap-8"
|
||||
>
|
||||
<h2 className="text-2xl">
|
||||
{isVariantSet ? langui.variants : langui.subitems}
|
||||
</h2>
|
||||
{item.gallery && item.gallery.data.length > 0 && (
|
||||
<div id="gallery" className="grid w-full place-items-center gap-8">
|
||||
<h2 className="text-2xl">{langui.gallery}</h2>
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
|
||||
gap-8"
|
||||
>
|
||||
{filterHasAttributes(item.gallery.data).map(
|
||||
(galleryItem, index) => (
|
||||
<Fragment key={galleryItem.id}>
|
||||
<div
|
||||
className="relative aspect-square cursor-pointer
|
||||
transition-transform hover:scale-[1.02]"
|
||||
onClick={() => {
|
||||
const images: string[] = filterHasAttributes(
|
||||
item.gallery?.data
|
||||
).map((image) =>
|
||||
getAssetURL(
|
||||
image.attributes.url,
|
||||
ImageQuality.Large
|
||||
)
|
||||
);
|
||||
openLightBox(images, index);
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
className="h-full w-full rounded-lg
|
||||
bg-light object-cover drop-shadow-shade-md"
|
||||
image={galleryItem.attributes}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch
|
||||
setState={setKeepInfoVisible}
|
||||
state={keepInfoVisible}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<InsetBox id="details" className="grid place-items-center">
|
||||
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-8">
|
||||
<h2 className="text-center text-2xl">{langui.details}</h2>
|
||||
<div
|
||||
className="grid place-items-center gap-y-8
|
||||
desktop:grid-flow-col desktop: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>{prettyItemType(item.metadata[0], langui)}</Chip>
|
||||
{"›"}
|
||||
<Chip>{prettyItemSubType(item.metadata[0])}</Chip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]
|
||||
items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1"
|
||||
>
|
||||
{filterHasAttributes(item.subitems.data).map((subitem) => (
|
||||
<Fragment key={subitem.id}>
|
||||
<PreviewCard
|
||||
href={`/library/${subitem.attributes.slug}`}
|
||||
title={subitem.attributes.title}
|
||||
subtitle={subitem.attributes.subtitle}
|
||||
thumbnail={subitem.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
subitem.attributes.metadata &&
|
||||
subitem.attributes.metadata.length > 0 &&
|
||||
subitem.attributes.metadata[0]
|
||||
? [prettyItemSubType(subitem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={subitem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
{item.release_date && (
|
||||
<div className="grid place-content-start place-items-center">
|
||||
<h3 className="text-xl">{langui.release_date}</h3>
|
||||
<p>{prettyDate(item.release_date)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.price && (
|
||||
<div className="grid place-content-start place-items-center text-center">
|
||||
<h3 className="text-xl">{langui.price}</h3>
|
||||
<p>
|
||||
{prettyPrice(
|
||||
item.price,
|
||||
currencies,
|
||||
item.price.currency?.data?.attributes?.code
|
||||
)}
|
||||
</p>
|
||||
{item.price.currency?.data?.attributes?.code !==
|
||||
appLayout.currency && (
|
||||
<p>
|
||||
{prettyPrice(
|
||||
item.price,
|
||||
currencies,
|
||||
appLayout.currency
|
||||
)}{" "}
|
||||
<br />({langui.calculated?.toLowerCase()})
|
||||
</p>
|
||||
)}
|
||||
metadata={{
|
||||
currencies: currencies,
|
||||
release_date: subitem.attributes.release_date,
|
||||
price: subitem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
<PreviewCardCTAs
|
||||
id={subitem.id}
|
||||
langui={langui}
|
||||
displayCTAs={
|
||||
!isUntangibleGroupItem(
|
||||
subitem.attributes.metadata?.[0]
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item?.contents && item.contents.data.length > 0 && (
|
||||
<div id="contents" className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
|
||||
{displayOpenScans && (
|
||||
<Button
|
||||
href={`/library/${item.slug}/scans`}
|
||||
text={langui.view_scans}
|
||||
/>
|
||||
)}
|
||||
<div className="grid w-full gap-4">
|
||||
{item.contents.data.map((content) => (
|
||||
<ContentLine
|
||||
langui={langui}
|
||||
content={content}
|
||||
parentSlug={item.slug}
|
||||
key={content.id}
|
||||
/>
|
||||
))}
|
||||
{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">
|
||||
{item.categories.data.map((category) => (
|
||||
<Chip key={category.id}>{category.attributes?.name}</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.size && (
|
||||
<div className="grid gap-8 mobile:place-items-center">
|
||||
<h3 className="text-xl">{langui.size}</h3>
|
||||
<div
|
||||
className="grid w-full grid-flow-col place-content-between thin:grid-flow-row
|
||||
thin:place-content-center thin:gap-8"
|
||||
>
|
||||
<div
|
||||
className="grid place-items-center gap-x-4 desktop:grid-flow-col
|
||||
desktop:place-items-start"
|
||||
>
|
||||
<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="grid place-items-center gap-x-4 desktop:grid-flow-col
|
||||
desktop:place-items-start"
|
||||
>
|
||||
<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="grid place-items-center gap-x-4 desktop:grid-flow-col
|
||||
desktop:place-items-start"
|
||||
>
|
||||
<p className="font-bold">{langui.thickness}:</p>
|
||||
<div>
|
||||
<p>{item.size.thickness} mm</p>
|
||||
<p>{convertMmToInch(item.size.thickness)} in</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
|
||||
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
|
||||
<>
|
||||
<h3 className="text-xl">{langui.type_information}</h3>
|
||||
<div className="grid w-full grid-cols-2 place-content-between">
|
||||
{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">
|
||||
<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>
|
||||
</ContentPanel>
|
||||
</InsetBox>
|
||||
|
||||
{item.subitems && item.subitems.data.length > 0 && (
|
||||
<div
|
||||
id={isVariantSet ? "variants" : "subitems"}
|
||||
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}
|
||||
input={
|
||||
<Switch
|
||||
setState={setKeepInfoVisible}
|
||||
state={keepInfoVisible}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]
|
||||
items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1"
|
||||
>
|
||||
{filterHasAttributes(item.subitems.data).map((subitem) => (
|
||||
<Fragment key={subitem.id}>
|
||||
<PreviewCard
|
||||
href={`/library/${subitem.attributes.slug}`}
|
||||
title={subitem.attributes.title}
|
||||
subtitle={subitem.attributes.subtitle}
|
||||
thumbnail={subitem.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
subitem.attributes.metadata &&
|
||||
subitem.attributes.metadata.length > 0 &&
|
||||
subitem.attributes.metadata[0]
|
||||
? [prettyItemSubType(subitem.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={subitem.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
currencies: currencies,
|
||||
release_date: subitem.attributes.release_date,
|
||||
price: subitem.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
<PreviewCardCTAs
|
||||
id={subitem.id}
|
||||
langui={langui}
|
||||
displayCTAs={
|
||||
!isUntangibleGroupItem(
|
||||
subitem.attributes.metadata?.[0]
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
<div id="contents" className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
|
||||
{displayOpenScans && (
|
||||
<Button
|
||||
href={`/library/${item.slug}/scans`}
|
||||
text={langui.view_scans}
|
||||
/>
|
||||
)}
|
||||
<div className="grid w-full gap-4">
|
||||
{item.contents.data.map((content) => (
|
||||
<ContentLine
|
||||
langui={langui}
|
||||
content={content}
|
||||
parentSlug={item.slug}
|
||||
key={content.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LightBox,
|
||||
openLightBox,
|
||||
appLayout.currency,
|
||||
currencies,
|
||||
displayOpenScans,
|
||||
hoverable,
|
||||
isVariantSet,
|
||||
item,
|
||||
itemId,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle={prettyinlineTitle("", item?.title, item?.subtitle)}
|
||||
navTitle={prettyinlineTitle("", item.title, item.subtitle)}
|
||||
contentPanel={contentPanel}
|
||||
subPanel={subPanel}
|
||||
thumbnail={item?.thumbnail?.data?.attributes ?? undefined}
|
||||
description={item?.descriptions?.[0]?.description ?? undefined}
|
||||
thumbnail={item.thumbnail?.data?.attributes ?? undefined}
|
||||
description={item.descriptions?.[0]?.description ?? undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -526,6 +554,7 @@ export async function getStaticProps(
|
|||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };
|
||||
sortContent(item.libraryItems.data[0].attributes.contents);
|
||||
const props: Props = {
|
||||
...(await getAppStaticProps(context)),
|
||||
item: item.libraryItems.data[0].attributes,
|
||||
|
@ -544,7 +573,7 @@ export async function getStaticPaths(
|
|||
const paths: GetStaticPathsResult["paths"] = [];
|
||||
filterHasAttributes(libraryItems.libraryItems?.data).map((item) => {
|
||||
context.locales?.map((local) =>
|
||||
paths.push({ params: { slug: item.attributes?.slug }, locale: local })
|
||||
paths.push({ params: { slug: item.attributes.slug }, locale: local })
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -23,96 +23,112 @@ import {
|
|||
GetStaticPathsResult,
|
||||
GetStaticPropsContext,
|
||||
} from "next";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
item: NonNullable<
|
||||
GetLibraryItemScansQuery["libraryItems"]
|
||||
>["data"][number]["attributes"];
|
||||
NonNullable<
|
||||
GetLibraryItemScansQuery["libraryItems"]
|
||||
>["data"][number]["attributes"]
|
||||
>;
|
||||
itemId: NonNullable<
|
||||
GetLibraryItemScansQuery["libraryItems"]
|
||||
>["data"][number]["id"];
|
||||
NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["id"]
|
||||
>;
|
||||
}
|
||||
|
||||
export default function LibrarySlug(props: Props): JSX.Element {
|
||||
const { item, langui, languages } = props;
|
||||
const [openLightBox, LightBox] = useLightBox();
|
||||
sortContent(item?.contents);
|
||||
sortContent(item.contents);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/library/${item?.slug}`}
|
||||
title={langui.item}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
|
||||
{item?.contents?.data.map((content) => (
|
||||
<NavOption
|
||||
key={content.id}
|
||||
url={`#${content.attributes?.slug}`}
|
||||
title={prettySlug(content.attributes?.slug, item.slug)}
|
||||
subtitle={
|
||||
content.attributes?.range[0]?.__typename ===
|
||||
"ComponentRangePageRange"
|
||||
? `${content.attributes.range[0].starting_page}` +
|
||||
`→` +
|
||||
`${content.attributes.range[0].ending_page}`
|
||||
: undefined
|
||||
}
|
||||
border
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/library/${item.slug}`}
|
||||
title={langui.item}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
))}
|
||||
</SubPanel>
|
||||
|
||||
{item.contents?.data.map((content) => (
|
||||
<NavOption
|
||||
key={content.id}
|
||||
url={`#${content.attributes?.slug}`}
|
||||
title={prettySlug(content.attributes?.slug, item.slug)}
|
||||
subtitle={
|
||||
content.attributes?.range[0]?.__typename ===
|
||||
"ComponentRangePageRange"
|
||||
? `${content.attributes.range[0].starting_page}` +
|
||||
`→` +
|
||||
`${content.attributes.range[0].ending_page}`
|
||||
: undefined
|
||||
}
|
||||
border
|
||||
/>
|
||||
))}
|
||||
</SubPanel>
|
||||
),
|
||||
[item.contents?.data, item.slug, langui]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<LightBox />
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<LightBox />
|
||||
|
||||
<ReturnButton
|
||||
href={`/library/${item?.slug}`}
|
||||
title={langui.item}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{item?.images && (
|
||||
<ScanSetCover
|
||||
images={item.images}
|
||||
openLightBox={openLightBox}
|
||||
languages={languages}
|
||||
<ReturnButton
|
||||
href={`/library/${item.slug}`}
|
||||
title={langui.item}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
)}
|
||||
|
||||
{item?.contents?.data.map((content) => (
|
||||
<Fragment key={content.id}>
|
||||
{content.attributes?.scan_set?.[0] && (
|
||||
<ScanSet
|
||||
scanSet={content.attributes.scan_set}
|
||||
openLightBox={openLightBox}
|
||||
slug={content.attributes.slug}
|
||||
title={prettySlug(content.attributes.slug, item.slug)}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
content={content.attributes.content}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
{item.images && (
|
||||
<ScanSetCover
|
||||
images={item.images}
|
||||
openLightBox={openLightBox}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
/>
|
||||
)}
|
||||
|
||||
{item.contents?.data.map((content) => (
|
||||
<Fragment key={content.id}>
|
||||
{content.attributes?.scan_set?.[0] && (
|
||||
<ScanSet
|
||||
scanSet={content.attributes.scan_set}
|
||||
openLightBox={openLightBox}
|
||||
slug={content.attributes.slug}
|
||||
title={prettySlug(content.attributes.slug, item.slug)}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
content={content.attributes.content}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LightBox,
|
||||
openLightBox,
|
||||
item.contents?.data,
|
||||
item.images,
|
||||
item.slug,
|
||||
languages,
|
||||
langui,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle={prettyinlineTitle("", item?.title, item?.subtitle)}
|
||||
navTitle={prettyinlineTitle("", item.title, item.subtitle)}
|
||||
contentPanel={contentPanel}
|
||||
subPanel={subPanel}
|
||||
thumbnail={item?.thumbnail?.data?.attributes ?? undefined}
|
||||
thumbnail={item.thumbnail?.data?.attributes ?? undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -129,7 +145,8 @@ export async function getStaticProps(
|
|||
: "",
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };
|
||||
if (!item.libraryItems?.data[0]?.attributes || !item.libraryItems.data[0]?.id)
|
||||
return { notFound: true };
|
||||
const props: Props = {
|
||||
...(await getAppStaticProps(context)),
|
||||
item: item.libraryItems.data[0].attributes,
|
||||
|
|
|
@ -114,204 +114,226 @@ export default function Library(props: Props): JSX.Element {
|
|||
[langui, groupingMethod, sortedItems]
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.LibraryBooks}
|
||||
title={langui.library}
|
||||
description={langui.library_description}
|
||||
/>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.LibraryBooks}
|
||||
title={langui.library}
|
||||
description={langui.library_description}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.group_by}
|
||||
input={
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.category ?? "Category",
|
||||
langui.type ?? "Type",
|
||||
langui.release_year ?? "Year",
|
||||
]}
|
||||
state={groupingMethod}
|
||||
setState={setGroupingMethod}
|
||||
allowEmpty
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.order_by}
|
||||
input={
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.name ?? "Name",
|
||||
langui.price ?? "Price",
|
||||
langui.release_date ?? "Release date",
|
||||
]}
|
||||
state={sortingMethod}
|
||||
setState={setSortingMethod}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.show_subitems}
|
||||
input={<Switch state={showSubitems} setState={setShowSubitems} />}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.show_primary_items}
|
||||
input={
|
||||
<Switch state={showPrimaryItems} setState={setShowPrimaryItems} />
|
||||
}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.show_secondary_items}
|
||||
input={
|
||||
<Switch state={showSecondaryItems} setState={setShowSecondaryItems} />
|
||||
}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
label={langui.group_by}
|
||||
input={
|
||||
<Switch state={keepInfoVisible} setState={setKeepInfoVisible} />
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.category ?? "Category",
|
||||
langui.type ?? "Type",
|
||||
langui.release_year ?? "Year",
|
||||
]}
|
||||
state={groupingMethod}
|
||||
setState={setGroupingMethod}
|
||||
allowEmpty
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ButtonGroup className="mt-4">
|
||||
<ToolTip content={langui.only_display_items_i_want}>
|
||||
<Button
|
||||
icon={Icon.Favorite}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.Want}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.only_display_items_i_have}>
|
||||
<Button
|
||||
icon={Icon.BackHand}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.Have}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.only_display_unmarked_items}>
|
||||
<Button
|
||||
icon={Icon.RadioButtonUnchecked}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.None)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.None}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.display_all_items}>
|
||||
<Button
|
||||
text={"All"}
|
||||
onClick={() => setFilterUserStatus(undefined)}
|
||||
active={isUndefined(filterUserStatus)}
|
||||
/>
|
||||
</ToolTip>
|
||||
</ButtonGroup>
|
||||
<WithLabel
|
||||
label={langui.order_by}
|
||||
input={
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
langui.name ?? "Name",
|
||||
langui.price ?? "Price",
|
||||
langui.release_date ?? "Release date",
|
||||
]}
|
||||
state={sortingMethod}
|
||||
setState={setSortingMethod}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setShowSubitems(defaultFiltersState.showSubitems);
|
||||
setShowPrimaryItems(defaultFiltersState.showPrimaryItems);
|
||||
setShowSecondaryItems(defaultFiltersState.showSecondaryItems);
|
||||
setSortingMethod(defaultFiltersState.sortingMethod);
|
||||
setGroupingMethod(defaultFiltersState.groupingMethod);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
setFilterUserStatus(defaultFiltersState.filterUserStatus);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
<WithLabel
|
||||
label={langui.show_subitems}
|
||||
input={<Switch state={showSubitems} setState={setShowSubitems} />}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.show_primary_items}
|
||||
input={
|
||||
<Switch state={showPrimaryItems} setState={setShowPrimaryItems} />
|
||||
}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.show_secondary_items}
|
||||
input={
|
||||
<Switch
|
||||
state={showSecondaryItems}
|
||||
setState={setShowSecondaryItems}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch state={keepInfoVisible} setState={setKeepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ButtonGroup className="mt-4">
|
||||
<ToolTip content={langui.only_display_items_i_want}>
|
||||
<Button
|
||||
icon={Icon.Favorite}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.Want}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.only_display_items_i_have}>
|
||||
<Button
|
||||
icon={Icon.BackHand}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.Have}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.only_display_unmarked_items}>
|
||||
<Button
|
||||
icon={Icon.RadioButtonUnchecked}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.None)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.None}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content={langui.display_all_items}>
|
||||
<Button
|
||||
text={"All"}
|
||||
onClick={() => setFilterUserStatus(undefined)}
|
||||
active={isUndefined(filterUserStatus)}
|
||||
/>
|
||||
</ToolTip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setShowSubitems(defaultFiltersState.showSubitems);
|
||||
setShowPrimaryItems(defaultFiltersState.showPrimaryItems);
|
||||
setShowSecondaryItems(defaultFiltersState.showSecondaryItems);
|
||||
setSortingMethod(defaultFiltersState.sortingMethod);
|
||||
setGroupingMethod(defaultFiltersState.groupingMethod);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
setFilterUserStatus(defaultFiltersState.filterUserStatus);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
filterUserStatus,
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
searchName,
|
||||
showPrimaryItems,
|
||||
showSecondaryItems,
|
||||
showSubitems,
|
||||
sortingMethod,
|
||||
]
|
||||
);
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{/* TODO: Add to langui */}
|
||||
{groups.size === 0 && (
|
||||
<ContentPlaceholder
|
||||
message={
|
||||
"No results. You can try changing or resetting the search parameters."
|
||||
}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
)}
|
||||
{iterateMap(groups, (name, items) => (
|
||||
<Fragment key={name}>
|
||||
{isDefinedAndNotEmpty(name) && (
|
||||
<h2
|
||||
className="flex flex-row place-items-center gap-2
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
{/* TODO: Add to langui */}
|
||||
{groups.size === 0 && (
|
||||
<ContentPlaceholder
|
||||
message={
|
||||
"No results. You can try changing or resetting the search parameters."
|
||||
}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
)}
|
||||
{iterateMap(groups, (name, items) => (
|
||||
<Fragment key={name}>
|
||||
{isDefinedAndNotEmpty(name) && (
|
||||
<h2
|
||||
className="flex flex-row place-items-center gap-2
|
||||
pb-2 pt-10 text-2xl first-of-type:pt-0"
|
||||
>
|
||||
{name}
|
||||
<Chip>{`${items.length} ${
|
||||
items.length <= 1
|
||||
? langui.result?.toLowerCase() ?? "result"
|
||||
: langui.results?.toLowerCase() ?? "results"
|
||||
}`}</Chip>
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className="grid items-end gap-8 border-b-[3px] border-dotted pb-12
|
||||
>
|
||||
{name}
|
||||
<Chip>{`${items.length} ${
|
||||
items.length <= 1
|
||||
? langui.result?.toLowerCase() ?? "result"
|
||||
: langui.results?.toLowerCase() ?? "results"
|
||||
}`}</Chip>
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className="grid items-end gap-8 border-b-[3px] border-dotted pb-12
|
||||
last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]
|
||||
mobile:grid-cols-2 mobile:gap-4"
|
||||
>
|
||||
{filterHasAttributes(items).map((item) => (
|
||||
<Fragment key={item.id}>
|
||||
<PreviewCard
|
||||
href={`/library/${item.attributes.slug}`}
|
||||
title={item.attributes.title}
|
||||
subtitle={item.attributes.subtitle}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
item.attributes.metadata &&
|
||||
item.attributes.metadata.length > 0 &&
|
||||
item.attributes.metadata[0]
|
||||
? [prettyItemSubType(item.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
currencies: currencies,
|
||||
release_date: item.attributes.release_date,
|
||||
price: item.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
<PreviewCardCTAs
|
||||
id={item.id}
|
||||
displayCTAs={
|
||||
!isUntangibleGroupItem(item.attributes.metadata?.[0])
|
||||
}
|
||||
langui={langui}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
>
|
||||
{filterHasAttributes(items).map((item) => (
|
||||
<Fragment key={item.id}>
|
||||
<PreviewCard
|
||||
href={`/library/${item.attributes.slug}`}
|
||||
title={item.attributes.title}
|
||||
subtitle={item.attributes.subtitle}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
topChips={
|
||||
item.attributes.metadata &&
|
||||
item.attributes.metadata.length > 0 &&
|
||||
item.attributes.metadata[0]
|
||||
? [prettyItemSubType(item.attributes.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
metadata={{
|
||||
currencies: currencies,
|
||||
release_date: item.attributes.release_date,
|
||||
price: item.attributes.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
<PreviewCardCTAs
|
||||
id={item.id}
|
||||
displayCTAs={
|
||||
!isUntangibleGroupItem(item.attributes.metadata?.[0])
|
||||
}
|
||||
langui={langui}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[currencies, groups, keepInfoVisible, langui]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
navTitle={langui.library}
|
||||
|
|
|
@ -5,20 +5,24 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
|||
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { Icon } from "components/Ico";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {}
|
||||
export default function Merch(props: Props): JSX.Element {
|
||||
const { langui } = props;
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Store}
|
||||
title={langui.merch}
|
||||
description={langui.merch_description}
|
||||
/>
|
||||
</SubPanel>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Store}
|
||||
title={langui.merch}
|
||||
description={langui.merch_description}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
|
||||
|
||||
return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,73 +46,79 @@ export default function News(props: Props): JSX.Element {
|
|||
[posts, searchName]
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Feed}
|
||||
title={langui.news}
|
||||
description={langui.news_description}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Feed}
|
||||
title={langui.news}
|
||||
description={langui.news_description}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui, searchName]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div
|
||||
className="grid grid-cols-1 items-end gap-8
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div
|
||||
className="grid grid-cols-1 items-end gap-8
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"
|
||||
>
|
||||
{filterHasAttributes(filteredItems).map((post) => (
|
||||
<Fragment key={post.id}>
|
||||
<PreviewCard
|
||||
href={`/news/${post.attributes.slug}`}
|
||||
title={
|
||||
post.attributes.translations?.[0]?.title ??
|
||||
prettySlug(post.attributes.slug)
|
||||
}
|
||||
description={post.attributes.translations?.[0]?.excerpt}
|
||||
thumbnail={post.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
bottomChips={post.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
release_date: post.attributes.date,
|
||||
position: "Top",
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
>
|
||||
{filterHasAttributes(filteredItems).map((post) => (
|
||||
<Fragment key={post.id}>
|
||||
<PreviewCard
|
||||
href={`/news/${post.attributes.slug}`}
|
||||
title={
|
||||
post.attributes.translations?.[0]?.title ??
|
||||
prettySlug(post.attributes.slug)
|
||||
}
|
||||
description={post.attributes.translations?.[0]?.excerpt}
|
||||
thumbnail={post.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
bottomChips={post.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
release_date: post.attributes.date,
|
||||
position: "Top",
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[filteredItems, keepInfoVisible]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
GetStaticPathsResult,
|
||||
GetStaticPropsContext,
|
||||
} from "next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
page: WikiPageWithTranslations;
|
||||
|
@ -40,80 +41,95 @@ export default function WikiPage(props: Props): JSX.Element {
|
|||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||
});
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/wiki`}
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
</SubPanel>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/wiki`}
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[langui]
|
||||
);
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Large}>
|
||||
<ReturnButton
|
||||
href={`/wiki`}
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="flex place-content-center gap-4">
|
||||
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Large}>
|
||||
<ReturnButton
|
||||
href={`/wiki`}
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
<div className="flex place-content-center gap-4">
|
||||
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
{selectedTranslation && (
|
||||
<div className="text-justify">
|
||||
<div
|
||||
className="float-right ml-8 mb-8 w-[25rem] overflow-hidden rounded-lg bg-mid
|
||||
<HorizontalLine />
|
||||
|
||||
{selectedTranslation && (
|
||||
<div className="text-justify">
|
||||
<div
|
||||
className="float-right ml-8 mb-8 w-[25rem] overflow-hidden rounded-lg bg-mid
|
||||
text-center"
|
||||
>
|
||||
{page.thumbnail?.data?.attributes && (
|
||||
<Img image={page.thumbnail.data.attributes} />
|
||||
)}
|
||||
<div className="my-4 grid gap-4 p-4">
|
||||
<p className="font-headers text-xl">{langui.categories}</p>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{page.categories?.data.map((category) => (
|
||||
<Chip key={category.id}>{category.attributes?.name}</Chip>
|
||||
))}
|
||||
>
|
||||
{page.thumbnail?.data?.attributes && (
|
||||
<Img image={page.thumbnail.data.attributes} />
|
||||
)}
|
||||
<div className="my-4 grid gap-4 p-4">
|
||||
<p className="font-headers text-xl">{langui.categories}</p>
|
||||
<div className="flex flex-row flex-wrap place-content-center gap-2">
|
||||
{page.categories?.data.map((category) => (
|
||||
<Chip key={category.id}>{category.attributes?.name}</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isDefinedAndNotEmpty(selectedTranslation.summary) && (
|
||||
<div className="mb-6">
|
||||
<p className="font-headers text-lg">{langui.summary}</p>
|
||||
<p>{selectedTranslation.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
{isDefinedAndNotEmpty(selectedTranslation.summary) && (
|
||||
<div className="mb-6">
|
||||
<p className="font-headers text-lg">{langui.summary}</p>
|
||||
<p>{selectedTranslation.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterHasAttributes(page.definitions, ["translations"]).map(
|
||||
(definition, index) => (
|
||||
<DefinitionCard
|
||||
key={index}
|
||||
source={definition.source?.data?.attributes?.name}
|
||||
translations={filterHasAttributes(definition.translations).map(
|
||||
(translation) => ({
|
||||
{filterHasAttributes(page.definitions, ["translations"]).map(
|
||||
(definition, index) => (
|
||||
<DefinitionCard
|
||||
key={index}
|
||||
source={definition.source?.data?.attributes?.name}
|
||||
translations={filterHasAttributes(
|
||||
definition.translations
|
||||
).map((translation) => ({
|
||||
language: translation.language.data?.attributes?.code,
|
||||
definition: translation.definition,
|
||||
status: translation.status,
|
||||
})
|
||||
)}
|
||||
index={index + 1}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ContentPanel>
|
||||
}))}
|
||||
index={index + 1}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
LanguageSwitcher,
|
||||
languages,
|
||||
langui,
|
||||
page.categories?.data,
|
||||
page.definitions,
|
||||
page.thumbnail?.data?.attributes,
|
||||
selectedTranslation,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -14,7 +14,7 @@ import { getReadySdk } from "graphql/sdk";
|
|||
import { prettySlug } from "helpers/formatters";
|
||||
import { filterHasAttributes, isDefined } from "helpers/others";
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useMemo } from "react";
|
||||
|
||||
interface Props extends AppStaticProps {
|
||||
chronologyItems: NonNullable<
|
||||
|
@ -27,109 +27,113 @@ export default function Chronology(props: Props): JSX.Element {
|
|||
const { chronologyItems, chronologyEras, langui } = props;
|
||||
|
||||
// Group by year the Chronology items
|
||||
const chronologyItemYearGroups: Props["chronologyItems"][number][][][] = [];
|
||||
const chronologyItemYearGroups = useMemo(() => {
|
||||
const memo: Props["chronologyItems"][number][][][] = [];
|
||||
chronologyEras.map(() => {
|
||||
memo.push([]);
|
||||
});
|
||||
|
||||
chronologyEras.map(() => {
|
||||
chronologyItemYearGroups.push([]);
|
||||
});
|
||||
|
||||
let currentChronologyEraIndex = 0;
|
||||
chronologyItems.map((item) => {
|
||||
if (item.attributes) {
|
||||
if (
|
||||
item.attributes.year >
|
||||
(chronologyEras[currentChronologyEraIndex].attributes?.ending_year ??
|
||||
999999)
|
||||
) {
|
||||
currentChronologyEraIndex += 1;
|
||||
let currentChronologyEraIndex = 0;
|
||||
chronologyItems.map((item) => {
|
||||
if (item.attributes) {
|
||||
if (
|
||||
item.attributes.year >
|
||||
(chronologyEras[currentChronologyEraIndex].attributes?.ending_year ??
|
||||
999999)
|
||||
) {
|
||||
currentChronologyEraIndex += 1;
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
memo[currentChronologyEraIndex],
|
||||
item.attributes.year
|
||||
)
|
||||
) {
|
||||
memo[currentChronologyEraIndex][item.attributes.year].push(item);
|
||||
} else {
|
||||
memo[currentChronologyEraIndex][item.attributes.year] = [item];
|
||||
}
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
chronologyItemYearGroups[currentChronologyEraIndex],
|
||||
item.attributes.year
|
||||
)
|
||||
) {
|
||||
chronologyItemYearGroups[currentChronologyEraIndex][
|
||||
item.attributes.year
|
||||
].push(item);
|
||||
} else {
|
||||
chronologyItemYearGroups[currentChronologyEraIndex][
|
||||
item.attributes.year
|
||||
] = [item];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return memo;
|
||||
}, [chronologyEras, chronologyItems]);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/wiki"
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href="/wiki"
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
|
||||
{filterHasAttributes(chronologyEras).map((era) => (
|
||||
<Fragment key={era.id}>
|
||||
<NavOption
|
||||
url={`#${era.attributes.slug}`}
|
||||
title={
|
||||
era.attributes.title &&
|
||||
era.attributes.title.length > 0 &&
|
||||
era.attributes.title[0]
|
||||
? era.attributes.title[0].title
|
||||
: prettySlug(era.attributes.slug)
|
||||
}
|
||||
subtitle={`${era.attributes.starting_year} → ${era.attributes.ending_year}`}
|
||||
border
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</SubPanel>
|
||||
{filterHasAttributes(chronologyEras).map((era) => (
|
||||
<Fragment key={era.id}>
|
||||
<NavOption
|
||||
url={`#${era.attributes.slug}`}
|
||||
title={
|
||||
era.attributes.title &&
|
||||
era.attributes.title.length > 0 &&
|
||||
era.attributes.title[0]
|
||||
? era.attributes.title[0].title
|
||||
: prettySlug(era.attributes.slug)
|
||||
}
|
||||
subtitle={`${era.attributes.starting_year} → ${era.attributes.ending_year}`}
|
||||
border
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</SubPanel>
|
||||
),
|
||||
[chronologyEras, langui]
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href="/wiki"
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href="/wiki"
|
||||
title={langui.wiki}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{chronologyItemYearGroups.map((era, eraIndex) => (
|
||||
<Fragment key={eraIndex}>
|
||||
<InsetBox
|
||||
id={chronologyEras[eraIndex].attributes?.slug}
|
||||
className="my-8 grid gap-4 text-center"
|
||||
>
|
||||
<h2 className="text-2xl">
|
||||
{chronologyEras[eraIndex].attributes?.title?.[0]
|
||||
? chronologyEras[eraIndex].attributes?.title?.[0]?.title
|
||||
: prettySlug(chronologyEras[eraIndex].attributes?.slug)}
|
||||
</h2>
|
||||
<p className="whitespace-pre-line ">
|
||||
{chronologyEras[eraIndex].attributes?.title?.[0]
|
||||
? chronologyEras[eraIndex].attributes?.title?.[0]?.description
|
||||
: ""}
|
||||
</p>
|
||||
</InsetBox>
|
||||
{era.map((items, index) => (
|
||||
<Fragment key={index}>
|
||||
{items[0].attributes && isDefined(items[0].attributes.year) && (
|
||||
<ChronologyYearComponent
|
||||
year={items[0].attributes.year}
|
||||
items={items}
|
||||
langui={langui}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
{chronologyItemYearGroups.map((era, eraIndex) => (
|
||||
<Fragment key={eraIndex}>
|
||||
<InsetBox
|
||||
id={chronologyEras[eraIndex].attributes?.slug}
|
||||
className="my-8 grid gap-4 text-center"
|
||||
>
|
||||
<h2 className="text-2xl">
|
||||
{chronologyEras[eraIndex].attributes?.title?.[0]
|
||||
? chronologyEras[eraIndex].attributes?.title?.[0]?.title
|
||||
: prettySlug(chronologyEras[eraIndex].attributes?.slug)}
|
||||
</h2>
|
||||
<p className="whitespace-pre-line ">
|
||||
{chronologyEras[eraIndex].attributes?.title?.[0]
|
||||
? chronologyEras[eraIndex].attributes?.title?.[0]?.description
|
||||
: ""}
|
||||
</p>
|
||||
</InsetBox>
|
||||
{era.map((items, index) => (
|
||||
<Fragment key={index}>
|
||||
{items[0].attributes && isDefined(items[0].attributes.year) && (
|
||||
<ChronologyYearComponent
|
||||
year={items[0].attributes.year}
|
||||
items={items}
|
||||
langui={langui}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[chronologyEras, chronologyItemYearGroups, langui]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -47,89 +47,95 @@ export default function Wiki(props: Props): JSX.Element {
|
|||
[pages, searchName]
|
||||
);
|
||||
|
||||
const subPanel = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.TravelExplore}
|
||||
title={langui.wiki}
|
||||
description={langui.wiki_description}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.TravelExplore}
|
||||
title={langui.wiki}
|
||||
description={langui.wiki_description}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
}}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? undefined}
|
||||
state={searchName}
|
||||
setState={setSearchName}
|
||||
/>
|
||||
|
||||
{/* TODO: Langui */}
|
||||
<p className="mb-4 font-headers text-xl">Special Pages</p>
|
||||
|
||||
<NavOption title={langui.chronology} url="/wiki/chronology" border />
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div
|
||||
className="grid grid-cols-2 items-end gap-8
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4"
|
||||
>
|
||||
{/* TODO: Add to langui */}
|
||||
{filteredPages.length === 0 && (
|
||||
<ContentPlaceholder
|
||||
message={
|
||||
"No results. You can try changing or resetting the search parameters."
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||||
}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
)}
|
||||
{filterHasAttributes(filteredPages).map((page) => (
|
||||
<Fragment key={page.id}>
|
||||
<TranslatedPreviewCard
|
||||
href={`/wiki/${page.attributes.slug}`}
|
||||
translations={page.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
title: translation?.title,
|
||||
description: translation?.summary,
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
)}
|
||||
thumbnail={page.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio={"4/3"}
|
||||
thumbnailRounded
|
||||
thumbnailForceAspectRatio
|
||||
languages={languages}
|
||||
slug={page.attributes.slug}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
bottomChips={page.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(defaultFiltersState.searchName);
|
||||
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
|
||||
}}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
|
||||
{/* TODO: Langui */}
|
||||
<p className="mb-4 font-headers text-xl">Special Pages</p>
|
||||
|
||||
<NavOption title={langui.chronology} url="/wiki/chronology" border />
|
||||
</SubPanel>
|
||||
),
|
||||
[hoverable, keepInfoVisible, langui, searchName]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div
|
||||
className="grid grid-cols-2 items-end gap-8
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4"
|
||||
>
|
||||
{/* TODO: Add to langui */}
|
||||
{filteredPages.length === 0 && (
|
||||
<ContentPlaceholder
|
||||
message={
|
||||
"No results. You can try changing or resetting the search parameters."
|
||||
}
|
||||
icon={Icon.ChevronLeft}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
)}
|
||||
{filterHasAttributes(filteredPages).map((page) => (
|
||||
<Fragment key={page.id}>
|
||||
<TranslatedPreviewCard
|
||||
href={`/wiki/${page.attributes.slug}`}
|
||||
translations={page.attributes.translations?.map(
|
||||
(translation) => ({
|
||||
title: translation?.title,
|
||||
description: translation?.summary,
|
||||
language: translation?.language?.data?.attributes?.code,
|
||||
})
|
||||
)}
|
||||
thumbnail={page.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio={"4/3"}
|
||||
thumbnailRounded
|
||||
thumbnailForceAspectRatio
|
||||
languages={languages}
|
||||
slug={page.attributes.slug}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
bottomChips={page.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
),
|
||||
[filteredPages, keepInfoVisible, languages]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
Loading…
Reference in New Issue