Continued using hooks

This commit is contained in:
DrMint 2022-06-23 00:39:59 +02:00
parent efcf01e8a0
commit d0b91f9db6
29 changed files with 2640 additions and 2356 deletions

View File

@ -161,30 +161,30 @@ export function AppLayout(props: Props): JSX.Element {
}, [fontSize]); }, [fontSize]);
const defaultPreferredLanguages = useMemo(() => { const defaultPreferredLanguages = useMemo(() => {
let list: string[] = []; let memo: string[] = [];
if (isDefinedAndNotEmpty(router.locale) && router.locales) { if (isDefinedAndNotEmpty(router.locale) && router.locales) {
if (router.locale === "en") { if (router.locale === "en") {
list = [router.locale]; memo = [router.locale];
router.locales.map((locale) => { router.locales.map((locale) => {
if (locale !== router.locale) list.push(locale); if (locale !== router.locale) memo.push(locale);
}); });
} else { } else {
list = [router.locale, "en"]; memo = [router.locale, "en"];
router.locales.map((locale) => { 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]); }, [router.locale, router.locales]);
const currencyOptions = useMemo(() => { const currencyOptions = useMemo(() => {
const list: string[] = []; const memo: string[] = [];
filterHasAttributes(currencies).map((currentCurrency) => { filterHasAttributes(currencies).map((currentCurrency) => {
if (isDefinedAndNotEmpty(currentCurrency.attributes.code)) if (isDefinedAndNotEmpty(currentCurrency.attributes.code))
list.push(currentCurrency.attributes.code); memo.push(currentCurrency.attributes.code);
}); });
return list; return memo;
}, [currencies]); }, [currencies]);
const [currencySelect, setCurrencySelect] = useState<number>(-1); const [currencySelect, setCurrencySelect] = useState<number>(-1);

View File

@ -157,12 +157,10 @@ export function ScanSet(props: Props): JSX.Element {
{filterHasAttributes(selectedScan.cleaners.data).map( {filterHasAttributes(selectedScan.cleaners.data).map(
(cleaner) => ( (cleaner) => (
<Fragment key={cleaner.id}> <Fragment key={cleaner.id}>
{cleaner.attributes && ( <RecorderChip
<RecorderChip langui={langui}
langui={langui} recorder={cleaner.attributes}
recorder={cleaner.attributes} />
/>
)}
</Fragment> </Fragment>
) )
)} )}
@ -178,12 +176,10 @@ export function ScanSet(props: Props): JSX.Element {
{filterHasAttributes(selectedScan.typesetters.data).map( {filterHasAttributes(selectedScan.typesetters.data).map(
(typesetter) => ( (typesetter) => (
<Fragment key={typesetter.id}> <Fragment key={typesetter.id}>
{typesetter.attributes && ( <RecorderChip
<RecorderChip langui={langui}
langui={langui} recorder={typesetter.attributes}
recorder={typesetter.attributes} />
/>
)}
</Fragment> </Fragment>
) )
)} )}
@ -218,9 +214,7 @@ export function ScanSet(props: Props): JSX.Element {
openLightBox(images, index); openLightBox(images, index);
}} }}
> >
{page.attributes && ( <Img image={page.attributes} quality={ImageQuality.Small} />
<Img image={page.attributes} quality={ImageQuality.Small} />
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -11,7 +11,7 @@ import { getAssetURL, ImageQuality } from "helpers/img";
import { filterHasAttributes, getStatusDescription } from "helpers/others"; import { filterHasAttributes, getStatusDescription } from "helpers/others";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
interface Props { interface Props {
openLightBox: (images: string[], index?: number) => void; openLightBox: (images: string[], index?: number) => void;
@ -35,19 +35,22 @@ export function ScanSetCover(props: Props): JSX.Element {
languageExtractor: (item) => item.language?.data?.attributes?.code, languageExtractor: (item) => item.language?.data?.attributes?.code,
}); });
const coverImages: UploadImageFragment[] = []; const coverImages = useMemo(() => {
if (selectedScan?.obi_belt?.full?.data?.attributes) const memo: UploadImageFragment[] = [];
coverImages.push(selectedScan.obi_belt.full.data.attributes); if (selectedScan?.obi_belt?.full?.data?.attributes)
if (selectedScan?.obi_belt?.inside_full?.data?.attributes) memo.push(selectedScan.obi_belt.full.data.attributes);
coverImages.push(selectedScan.obi_belt.inside_full.data.attributes); if (selectedScan?.obi_belt?.inside_full?.data?.attributes)
if (selectedScan?.dust_jacket?.full?.data?.attributes) memo.push(selectedScan.obi_belt.inside_full.data.attributes);
coverImages.push(selectedScan.dust_jacket.full.data.attributes); if (selectedScan?.dust_jacket?.full?.data?.attributes)
if (selectedScan?.dust_jacket?.inside_full?.data?.attributes) memo.push(selectedScan.dust_jacket.full.data.attributes);
coverImages.push(selectedScan.dust_jacket.inside_full.data.attributes); if (selectedScan?.dust_jacket?.inside_full?.data?.attributes)
if (selectedScan?.cover?.full?.data?.attributes) memo.push(selectedScan.dust_jacket.inside_full.data.attributes);
coverImages.push(selectedScan.cover.full.data.attributes); if (selectedScan?.cover?.full?.data?.attributes)
if (selectedScan?.cover?.inside_full?.data?.attributes) memo.push(selectedScan.cover.full.data.attributes);
coverImages.push(selectedScan.cover.inside_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) { if (coverImages.length > 0) {
return ( return (

View File

@ -67,105 +67,136 @@ export function PostPage(props: Props): JSX.Element {
[post.slug, post.thumbnail, selectedTranslation] [post.slug, post.thumbnail, selectedTranslation]
); );
const subPanel = const subPanel = useMemo(
returnHref || returnTitle || displayCredits || displayToc ? ( () =>
<SubPanel> 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 && ( {returnHref && returnTitle && (
<ReturnButton <ReturnButton
href={returnHref} href={returnHref}
title={returnTitle} title={returnTitle}
langui={langui} langui={langui}
displayOn={ReturnButtonType.Desktop} displayOn={ReturnButtonType.Mobile}
horizontalLine horizontalLine
/> />
)} )}
{displayCredits && ( {displayThumbnailHeader ? (
<> <>
{selectedTranslation && ( <ThumbnailHeader
<div className="grid grid-flow-col place-content-center place-items-center gap-2"> thumbnail={thumbnail}
<p className="font-headers">{langui.status}:</p> title={title}
description={excerpt}
<ToolTip langui={langui}
content={getStatusDescription( categories={post.categories}
selectedTranslation.status, languageSwitcher={<LanguageSwitcher />}
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 /> <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} />} {prependBody}
</SubPanel> <Markdawn text={body} />
) : undefined; {appendBody}
</ContentPanel>
const contentPanel = ( ),
<ContentPanel> [
{returnHref && returnTitle && ( LanguageSwitcher,
<ReturnButton appendBody,
href={returnHref} body,
title={returnTitle} displayLanguageSwitcher,
langui={langui} displayThumbnailHeader,
displayOn={ReturnButtonType.Mobile} displayTitle,
horizontalLine excerpt,
/> langui,
)} post.categories,
prependBody,
{displayThumbnailHeader ? ( returnHref,
<> returnTitle,
<ThumbnailHeader thumbnail,
thumbnail={thumbnail} title,
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>
); );
return ( return (

View File

@ -85,7 +85,6 @@ export function ChronologyItemComponent(props: Props): JSX.Element {
{translation.description && ( {translation.description && (
<p <p
className={ className={
event.translations &&
event.translations.length > 1 event.translations.length > 1
? `mt-2 whitespace-pre-line before:ml-[-1em] before:inline-block ? `mt-2 whitespace-pre-line before:ml-[-1em] before:inline-block
before:w-4 before:text-dark before:content-['-']` before:w-4 before:text-dark before:content-['-']`

View File

@ -208,7 +208,7 @@ export function sortBy(
orderByType: number, orderByType: number,
items: Items, items: Items,
currencies: AppStaticProps["currencies"] currencies: AppStaticProps["currencies"]
): Items { ) {
switch (orderByType) { switch (orderByType) {
case 0: case 0:
return items.sort((a, b) => { return items.sort((a, b) => {

View File

@ -19,23 +19,18 @@ type SortContentProps =
>["contents"]; >["contents"];
export function sortContent(contents: SortContentProps) { export function sortContent(contents: SortContentProps) {
if (contents) { contents?.data.sort((a, b) => {
const newContent = { ...contents }; if (
newContent?.data.sort((a, b) => { a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
if ( b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
a.attributes?.range[0]?.__typename === "ComponentRangePageRange" && ) {
b.attributes?.range[0]?.__typename === "ComponentRangePageRange" return (
) { a.attributes.range[0].starting_page -
return ( b.attributes.range[0].starting_page
a.attributes.range[0].starting_page - );
b.attributes.range[0].starting_page }
); return 0;
} });
return 0;
});
return newContent;
}
return contents;
} }
export function getStatusDescription( export function getStatusDescription(

View File

@ -38,12 +38,12 @@ export function useSmartLanguage<T>(
const router = useRouter(); const router = useRouter();
const availableLocales = useMemo(() => { const availableLocales = useMemo(() => {
const map = new Map<string, number>(); const memo = new Map<string, number>();
filterDefined(items).map((elem, index) => { filterDefined(items).map((elem, index) => {
const result = languageExtractor(elem); const result = languageExtractor(elem);
if (isDefined(result)) map.set(result, index); if (isDefined(result)) memo.set(result, index);
}); });
return map; return memo;
}, [items, languageExtractor]); }, [items, languageExtractor]);
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState< const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<

View File

@ -7,21 +7,25 @@ import { ContentPanel } from "components/Panels/ContentPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function FourOhFour(props: Props): JSX.Element { export default function FourOhFour(props: Props): JSX.Element {
const { langui } = props; const { langui } = props;
const contentPanel = ( const contentPanel = useMemo(
<ContentPanel> () => (
<h1>404 - {langui.page_not_found}</h1> <ContentPanel>
<ReturnButton <h1>404 - {langui.page_not_found}</h1>
href="/" <ReturnButton
title="Home" href="/"
langui={langui} title="Home"
displayOn={ReturnButtonType.Both} langui={langui}
/> displayOn={ReturnButtonType.Both}
</ContentPanel> />
</ContentPanel>
),
[langui]
); );
return <AppLayout navTitle="404" contentPanel={contentPanel} {...props} />; return <AppLayout navTitle="404" contentPanel={contentPanel} {...props} />;
} }

View File

@ -7,21 +7,25 @@ import { ContentPanel } from "components/Panels/ContentPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function FiveHundred(props: Props): JSX.Element { export default function FiveHundred(props: Props): JSX.Element {
const { langui } = props; const { langui } = props;
const contentPanel = ( const contentPanel = useMemo(
<ContentPanel> () => (
<h1>500 - Internal Server Error</h1> <ContentPanel>
<ReturnButton <h1>500 - Internal Server Error</h1>
href="/" <ReturnButton
title="Home" href="/"
langui={langui} title="Home"
displayOn={ReturnButtonType.Both} langui={langui}
/> displayOn={ReturnButtonType.Both}
</ContentPanel> />
</ContentPanel>
),
[langui]
); );
return <AppLayout navTitle="500" contentPanel={contentPanel} {...props} />; return <AppLayout navTitle="500" contentPanel={contentPanel} {...props} />;
} }

View File

@ -6,32 +6,35 @@ import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function AboutUs(props: Props): JSX.Element { export default function AboutUs(props: Props): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.Info} <PanelHeader
title={langui.about_us} icon={Icon.Info}
description={langui.about_us_description} title={langui.about_us}
/> description={langui.about_us_description}
<NavOption />
title={langui.accords_handbook} <NavOption
url="/about-us/accords-handbook" title={langui.accords_handbook}
border 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.legality} url="/about-us/legality" border />
<NavOption <NavOption
title={langui.sharing_policy} title={langui.sharing_policy}
url="/about-us/sharing-policy" url="/about-us/sharing-policy"
border border
/> />
<NavOption title={langui.contact_us} url="/about-us/contact" border /> <NavOption title={langui.contact_us} url="/about-us/contact" border />
</SubPanel> </SubPanel>
),
[langui]
); );
return ( return (
<AppLayout navTitle={langui.about_us} subPanel={subPanel} {...props} /> <AppLayout navTitle={langui.about_us} subPanel={subPanel} {...props} />

View File

@ -6,20 +6,24 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { useMemo } from "react";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Archives(props: Props): JSX.Element { export default function Archives(props: Props): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.Inventory} <PanelHeader
title={langui.archives} icon={Icon.Inventory}
description={langui.archives_description} title={langui.archives}
/> description={langui.archives_description}
<NavOption title={"Videos"} url="/archives/videos/" border /> />
</SubPanel> <NavOption title={"Videos"} url="/archives/videos/" border />
</SubPanel>
),
[langui]
); );
return ( return (
<AppLayout navTitle={langui.archives} subPanel={subPanel} {...props} /> <AppLayout navTitle={langui.archives} subPanel={subPanel} {...props} />

View File

@ -20,7 +20,7 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { Fragment, useState } from "react"; import { Fragment, useState, useMemo } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
@ -37,67 +37,79 @@ export default function Channel(props: Props): JSX.Element {
const [keepInfoVisible, setKeepInfoVisible] = useState(true); const [keepInfoVisible, setKeepInfoVisible] = useState(true);
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href="/archives/videos/" <ReturnButton
title={langui.videos} href="/archives/videos/"
langui={langui} title={langui.videos}
displayOn={ReturnButtonType.Desktop} langui={langui}
className="mb-10" 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} />
}
/> />
)}
</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 = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
<div className="mb-8"> <ContentPanel width={ContentPanelWidthSizes.Full}>
<h1 className="text-3xl">{channel?.title}</h1> <div className="mb-8">
<p>{channel?.subscribers.toLocaleString()} subscribers</p> <h1 className="text-3xl">{channel?.title}</h1>
</div> <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 <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" desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2"
> >
{filterHasAttributes(channel?.videos?.data).map((video) => ( {filterHasAttributes(channel?.videos?.data).map((video) => (
<Fragment key={video.id}> <Fragment key={video.id}>
<PreviewCard <PreviewCard
href={`/archives/videos/v/${video.attributes.uid}`} href={`/archives/videos/v/${video.attributes.uid}`}
title={video.attributes.title} title={video.attributes.title}
thumbnail={getVideoThumbnailURL(video.attributes.uid)} thumbnail={getVideoThumbnailURL(video.attributes.uid)}
thumbnailAspectRatio="16/9" thumbnailAspectRatio="16/9"
keepInfoVisible={keepInfoVisible} keepInfoVisible={keepInfoVisible}
metadata={{ metadata={{
release_date: video.attributes.published_date, release_date: video.attributes.published_date,
views: video.attributes.views, views: video.attributes.views,
author: channel?.title, author: channel?.title,
position: "Top", position: "Top",
}} }}
hoverlay={{ hoverlay={{
__typename: "Video", __typename: "Video",
duration: video.attributes.duration, duration: video.attributes.duration,
}} }}
/> />
</Fragment> </Fragment>
))} ))}
</div> </div>
</ContentPanel> </ContentPanel>
),
[
channel?.subscribers,
channel?.title,
channel?.videos?.data,
keepInfoVisible,
]
); );
return ( return (
<AppLayout <AppLayout
navTitle={langui.archives} navTitle={langui.archives}

View File

@ -22,110 +22,108 @@ import { filterHasAttributes } from "helpers/others";
import { getVideoThumbnailURL } from "helpers/videos"; import { getVideoThumbnailURL } from "helpers/videos";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment, useState } from "react"; import { Fragment, useMemo, useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"]; videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"];
} }
const ITEM_PER_PAGE = 50;
export default function Videos(props: Props): JSX.Element { export default function Videos(props: Props): JSX.Element {
const { langui, videos } = props; const { langui, videos } = props;
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
videos const paginatedVideos = useMemo(() => {
.sort((a, b) => { const memo = [];
const dateA = a.attributes?.published_date for (let index = 0; ITEM_PER_PAGE * index < videos.length; index += 1) {
? prettyDate(a.attributes.published_date) memo.push(
: "9999"; videos.slice(index * ITEM_PER_PAGE, (index + 1) * ITEM_PER_PAGE)
const dateB = b.attributes?.published_date );
? prettyDate(b.attributes.published_date) }
: "9999"; return memo;
return dateA.localeCompare(dateB); }, [videos]);
})
.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 [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [keepInfoVisible, setKeepInfoVisible] = useState(true); const [keepInfoVisible, setKeepInfoVisible] = useState(true);
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href="/archives/" <ReturnButton
title={"Archives"} href="/archives/"
langui={langui} title={"Archives"}
displayOn={ReturnButtonType.Desktop} langui={langui}
className="mb-10" 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} />
}
/> />
)}
</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 = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
<PageSelector <ContentPanel width={ContentPanelWidthSizes.Full}>
maxPage={Math.floor(videos.length / itemPerPage)} <PageSelector
page={page} maxPage={Math.floor(videos.length / ITEM_PER_PAGE)}
setPage={setPage} page={page}
className="mb-12" setPage={setPage}
/> className="mb-12"
/>
<div <div
className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 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 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2
thin:grid-cols-1" thin:grid-cols-1"
> >
{filterHasAttributes(paginatedVideos[page]).map((video) => ( {filterHasAttributes(paginatedVideos[page]).map((video) => (
<Fragment key={video.id}> <Fragment key={video.id}>
<PreviewCard <PreviewCard
href={`/archives/videos/v/${video.attributes.uid}`} href={`/archives/videos/v/${video.attributes.uid}`}
title={video.attributes.title} title={video.attributes.title}
thumbnail={getVideoThumbnailURL(video.attributes.uid)} thumbnail={getVideoThumbnailURL(video.attributes.uid)}
thumbnailAspectRatio="16/9" thumbnailAspectRatio="16/9"
keepInfoVisible={keepInfoVisible} keepInfoVisible={keepInfoVisible}
metadata={{ metadata={{
release_date: video.attributes.published_date, release_date: video.attributes.published_date,
views: video.attributes.views, views: video.attributes.views,
author: video.attributes.channel?.data?.attributes?.title, author: video.attributes.channel?.data?.attributes?.title,
position: "Top", position: "Top",
}} }}
hoverlay={{ hoverlay={{
__typename: "Video", __typename: "Video",
duration: video.attributes.duration, duration: video.attributes.duration,
}} }}
/> />
</Fragment> </Fragment>
))} ))}
</div> </div>
<PageSelector <PageSelector
maxPage={Math.floor(videos.length / itemPerPage)} maxPage={Math.floor(videos.length / ITEM_PER_PAGE)}
page={page} page={page}
setPage={setPage} setPage={setPage}
className="mt-12" className="mt-12"
/> />
</ContentPanel> </ContentPanel>
),
[keepInfoVisible, page, paginatedVideos, videos.length]
); );
return ( return (
<AppLayout <AppLayout
@ -143,6 +141,17 @@ export async function getStaticProps(
const sdk = getReadySdk(); const sdk = getReadySdk();
const videos = await sdk.getVideosPreview(); const videos = await sdk.getVideosPreview();
if (!videos.videos) return { notFound: true }; 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 = { const props: Props = {
...(await getAppStaticProps(context)), ...(await getAppStaticProps(context)),
videos: videos.videos.data, videos: videos.videos.data,

View File

@ -26,6 +26,7 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
video: NonNullable< video: NonNullable<
@ -37,145 +38,164 @@ export default function Video(props: Props): JSX.Element {
const { langui, video } = props; const { langui, video } = props;
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
const appLayout = useAppLayout(); const appLayout = useAppLayout();
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href="/archives/videos/" <ReturnButton
title={langui.videos} href="/archives/videos/"
langui={langui} title={langui.videos}
displayOn={ReturnButtonType.Desktop} langui={langui}
className="mb-10" displayOn={ReturnButtonType.Desktop}
/> className="mb-10"
/>
<HorizontalLine /> <HorizontalLine />
<NavOption <NavOption
title={langui.video} title={langui.video}
url="#video" url="#video"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
/> />
<NavOption <NavOption
title={langui.channel} title={langui.channel}
url="#channel" url="#channel"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
/> />
<NavOption <NavOption
title={langui.description} title={langui.description}
url="#description" url="#description"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
/> />
</SubPanel> </SubPanel>
),
[appLayout, langui]
); );
const contentPanel = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
<ReturnButton <ContentPanel width={ContentPanelWidthSizes.Full}>
href="/library/" <ReturnButton
title={langui.library} href="/library/"
langui={langui} title={langui.library}
displayOn={ReturnButtonType.Mobile} langui={langui}
className="mb-10" displayOn={ReturnButtonType.Mobile}
/> className="mb-10"
/>
<div className="grid place-items-center gap-12"> <div className="grid place-items-center gap-12">
<div <div
id="video" id="video"
className="w-full overflow-hidden rounded-xl shadow-lg shadow-shade" className="w-full overflow-hidden rounded-xl shadow-lg shadow-shade"
> >
{video.gone ? ( {video.gone ? (
<video <video
className="w-full" className="w-full"
src={getVideoFile(video.uid)} src={getVideoFile(video.uid)}
controls controls
></video> ></video>
) : ( ) : (
<iframe <iframe
src={`https://www.youtube-nocookie.com/embed/${video.uid}`} src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
className="aspect-video w-full" className="aspect-video w-full"
title="YouTube video player" title="YouTube video player"
frameBorder="0" frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; allow="accelerometer; autoplay; clipboard-write;
encrypted-media; gyroscope; picture-in-picture" encrypted-media; gyroscope; picture-in-picture"
allowFullScreen allowFullScreen
></iframe> ></iframe>
)} )}
<div className="mt-2 p-6"> <div className="mt-2 p-6">
<h1 className="text-2xl">{video.title}</h1> <h1 className="text-2xl">{video.title}</h1>
<div className="flex w-full flex-row flex-wrap gap-x-6"> <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 && (
<p> <p>
<Ico <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" className="mr-1 translate-y-[.15em] !text-base"
/> />
{isMobile {isMobile
? prettyShortenNumber(video.likes) ? prettyShortenNumber(video.views)
: video.likes.toLocaleString()} : video.views.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()}`}
</p> </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>
</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> </div>
</InsetBox>
</div> {video.channel?.data?.attributes && (
</ContentPanel> <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 ( return (
<AppLayout <AppLayout
navTitle={langui.archives} navTitle={langui.archives}

View File

@ -5,20 +5,25 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { useMemo } from "react";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Chronicles(props: Props): JSX.Element { export default function Chronicles(props: Props): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.WatchLater} <PanelHeader
title={langui.chronicles} icon={Icon.WatchLater}
description={langui.chronicles_description} title={langui.chronicles}
/> description={langui.chronicles_description}
</SubPanel> />
</SubPanel>
),
[langui]
); );
return ( return (
<AppLayout navTitle={langui.chronicles} subPanel={subPanel} {...props} /> <AppLayout navTitle={langui.chronicles} subPanel={subPanel} {...props} />
); );

View File

@ -76,321 +76,349 @@ export default function Content(props: Props): JSX.Element {
[content.group, content.slug] [content.group, content.slug]
); );
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href={`/contents`} <ReturnButton
title={langui.contents} href={`/contents`}
langui={langui} title={langui.contents}
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}
langui={langui} langui={langui}
languageSwitcher={<LanguageSwitcher />} displayOn={ReturnButtonType.Desktop}
horizontalLine
/> />
{previousContent?.attributes && ( {selectedTranslation?.text_set?.source_language?.data?.attributes
<div className="mt-12 mb-8 w-full"> ?.code !== undefined && (
<h2 className="mb-4 text-center text-2xl"> <div className="grid gap-5">
{langui.previous_content} <h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes
.code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
</h2> </h2>
<TranslatedPreviewLine
href={`/contents/${previousContent.attributes.slug}`} {selectedTranslation.text_set.source_language.data.attributes
translations={previousContent.attributes.translations?.map( .code !==
(translation) => ({ selectedTranslation.language?.data?.attributes?.code && (
pre_title: translation?.pre_title, <div className="grid place-items-center gap-2">
title: translation?.title, <p className="font-headers">{langui.source_language}:</p>
subtitle: translation?.subtitle, <Chip>
language: translation?.language?.data?.attributes?.code, {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} {selectedTranslation.text_set.translators &&
thumbnail={previousContent.attributes.thumbnail?.data?.attributes} selectedTranslation.text_set.translators.data.length > 0 && (
thumbnailAspectRatio="3/2" <div>
topChips={ <p className="font-headers">{langui.translators}:</p>
isMobile <div className="grid place-content-center place-items-center gap-2">
? undefined {filterHasAttributes(
: previousContent.attributes.type?.data?.attributes selectedTranslation.text_set.translators.data
? [ ).map((recorder) => (
previousContent.attributes.type.data.attributes <Fragment key={recorder.id}>
.titles?.[0] <RecorderChip
? previousContent.attributes.type.data.attributes langui={langui}
.titles[0]?.title recorder={recorder.attributes}
: prettySlug( />
previousContent.attributes.type.data.attributes.slug </Fragment>
), ))}
] </div>
: undefined </div>
} )}
bottomChips={
isMobile {selectedTranslation.text_set.proofreaders &&
? undefined selectedTranslation.text_set.proofreaders.data.length > 0 && (
: previousContent.attributes.categories?.data.map( <div>
(category) => category.attributes?.short ?? "" <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> </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 ?? ""} /> {selectedTranslation?.text_set?.text && (
{nextContent?.attributes && (
<> <>
<HorizontalLine /> <HorizontalLine />
<h2 className="mb-4 text-center text-2xl"> <TOC
{langui.followup_content} text={selectedTranslation.text_set.text}
</h2> title={prettyinlineTitle(
<TranslatedPreviewLine selectedTranslation.pre_title,
href={`/contents/${nextContent.attributes.slug}`} selectedTranslation.title,
translations={nextContent.attributes.translations?.map( selectedTranslation.subtitle
(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> </SubPanel>
</ContentPanel> ),
[
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 ( return (

View File

@ -71,166 +71,188 @@ export default function Contents(props: Props): JSX.Element {
[combineRelatedContent, searchName.length] [combineRelatedContent, searchName.length]
); );
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.Workspaces} <PanelHeader
title={langui.contents} icon={Icon.Workspaces}
description={langui.contents_description} title={langui.contents}
/> description={langui.contents_description}
/>
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} state={searchName}
setState={setSearchName} 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 <WithLabel
label={langui.always_show_info} label={langui.group_by}
input={ input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Select
className="w-full"
options={[langui.category ?? "", langui.type ?? ""]}
state={groupingMethod}
setState={setGroupingMethod}
allowEmpty
/>
} }
/> />
)}
<Button <WithLabel
className="mt-8" label={langui.combine_related_contents}
text={langui.reset_all_filters} disabled={searchName.length > 1}
icon={Icon.Replay} input={
onClick={() => { <Switch
setSearchName(defaultFiltersState.searchName); setState={setCombineRelatedContent}
setGroupingMethod(defaultFiltersState.groupingMethod); state={effectiveCombineRelatedContent}
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."
} }
icon={Icon.ChevronLeft}
/> />
)}
{iterateMap( {hoverable && (
groups, <WithLabel
(name, items, index) => label={langui.always_show_info}
items.length > 0 && ( input={
<Fragment key={index}> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
{name && ( }
<h2 />
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl )}
<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" first-of-type:pt-0"
> >
{name} {name}
<Chip>{`${items.reduce((currentSum, item) => { <Chip>{`${items.reduce((currentSum, item) => {
if (effectiveCombineRelatedContent) { if (effectiveCombineRelatedContent) {
if ( if (
item.attributes?.group?.data?.attributes?.combine === 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 ===
true true
? item.attributes.group.data.attributes.contents?.data ) {
.length return (
: 0 currentSum +
(item.attributes.group.data.attributes.contents
?.data.length ?? 1)
);
}
} }
topChips={ return currentSum + 1;
item.attributes.type?.data?.attributes }, 0)} ${
? [ items.length <= 1
item.attributes.type.data.attributes.titles?.[0] ? langui.result?.toLowerCase() ?? ""
? item.attributes.type.data.attributes.titles[0] : langui.results?.toLowerCase() ?? ""
?.title }`}</Chip>
: prettySlug( </h2>
item.attributes.type.data.attributes.slug )}
),
] <div
: undefined className="grid grid-cols-2 items-end gap-8
} desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4"
bottomChips={item.attributes.categories?.data.map( >
(category) => category.attributes?.short ?? "" {filterHasAttributes(items).map((item) => (
)} <Fragment key={item.id}>
keepInfoVisible={keepInfoVisible} <TranslatedPreviewCard
/> href={`/contents/${item.attributes.slug}`}
</Fragment> translations={item.attributes.translations?.map(
))} (translation) => ({
</div> pre_title: translation?.pre_title,
</Fragment> title: translation?.title,
) subtitle: translation?.subtitle,
)} language:
</ContentPanel> 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 ( return (
<AppLayout <AppLayout
navTitle={langui.contents} navTitle={langui.contents}

View File

@ -12,6 +12,7 @@ import { getReadySdk } from "graphql/sdk";
import { filterDefined, filterHasAttributes } from "helpers/others"; import { filterDefined, filterHasAttributes } from "helpers/others";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
contents: DevGetContentsQuery; contents: DevGetContentsQuery;
@ -21,61 +22,65 @@ export default function CheckupContents(props: Props): JSX.Element {
const { contents } = props; const { contents } = props;
const testReport = testingContent(contents); const testReport = testingContent(contents);
const contentPanel = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
{<h2 className="text-2xl">{testReport.title}</h2>} <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"> <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></p> <p></p>
<p className="font-headers">Ref</p> <p className="font-headers">Ref</p>
<p className="font-headers">Name</p> <p className="font-headers">Name</p>
<p className="font-headers">Type</p> <p className="font-headers">Type</p>
<p className="font-headers">Severity</p> <p className="font-headers">Severity</p>
<p className="font-headers">Description</p> <p className="font-headers">Description</p>
</div>
{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> </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 ( return (
<AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} /> <AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} />
); );

View File

@ -14,6 +14,7 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
libraryItems: DevGetLibraryItemsQuery; libraryItems: DevGetLibraryItemsQuery;
@ -23,61 +24,65 @@ export default function CheckupLibraryItems(props: Props): JSX.Element {
const { libraryItems } = props; const { libraryItems } = props;
const testReport = testingLibraryItem(libraryItems); const testReport = testingLibraryItem(libraryItems);
const contentPanel = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
{<h2 className="text-2xl">{testReport.title}</h2>} <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"> <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></p> <p></p>
<p className="font-headers">Ref</p> <p className="font-headers">Ref</p>
<p className="font-headers">Name</p> <p className="font-headers">Name</p>
<p className="font-headers">Type</p> <p className="font-headers">Type</p>
<p className="font-headers">Severity</p> <p className="font-headers">Severity</p>
<p className="font-headers">Description</p> <p className="font-headers">Description</p>
</div>
{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> </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 ( return (
<AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} /> <AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} />
); );

View File

@ -10,7 +10,7 @@ import { ToolTip } from "components/ToolTip";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useCallback, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import TurndownService from "turndown"; import TurndownService from "turndown";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { TOC } from "components/Markdown/TOC"; import { TOC } from "components/Markdown/TOC";
@ -25,408 +25,445 @@ export default function Editor(props: Props): JSX.Element {
const [markdown, setMarkdown] = useState(""); const [markdown, setMarkdown] = useState("");
const [converterOpened, setConverterOpened] = useState(false); const [converterOpened, setConverterOpened] = useState(false);
function wrap( const transformationWrapper = useCallback(
wrapper: string, (
properties?: Record<string, string>, transformation: (
addInnerNewLines?: boolean value: string,
) { selectionStart: number,
transformationWrapper((value, selectionStart, selectionEnd) => { selectedEnd: number
let prepend = wrapper; ) => { prependLength: number; transformedValue: string }
let append = wrapper; ) => {
const textarea =
document.querySelector<HTMLTextAreaElement>("#editorTextArea");
if (textarea) {
const { value, selectionStart, selectionEnd } = textarea;
if (properties) { const { prependLength, transformedValue } = transformation(
prepend = `<${wrapper}${Object.entries(properties).map( value,
([propertyName, propertyValue]) => selectionStart,
` ${propertyName}="${propertyValue}"` selectionEnd
)}>`; );
append = `</${wrapper}>`;
textarea.value = transformedValue;
handleInput(textarea.value);
textarea.focus();
textarea.selectionStart = selectionStart + prependLength;
textarea.selectionEnd = selectionEnd + prependLength;
} }
},
[handleInput]
);
if (addInnerNewLines === true) { const wrap = useCallback(
prepend = `${prepend}\n`; (
append = `\n${append}`; 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 = ""; const preline = useCallback(
newValue += value.slice(0, selectionStart); (prepend: string) => {
newValue += prepend; transformationWrapper((value, selectionStart) => {
newValue += value.slice(selectionStart, selectionEnd); const lastNewLine =
newValue += append; value.slice(0, selectionStart).lastIndexOf("\n") + 1;
newValue += value.slice(selectionEnd);
return { prependLength: prepend.length, transformedValue: newValue };
});
}
function toggleWrap( let newValue = "";
wrapper: string, newValue += value.slice(0, lastNewLine);
properties?: Record<string, string>, newValue += prepend;
addInnerNewLines?: boolean newValue += value.slice(lastNewLine);
) {
const textarea =
document.querySelector<HTMLTextAreaElement>("#editorTextArea");
if (textarea) {
const { value, selectionStart, selectionEnd } = textarea;
if ( return { prependLength: prepend.length, transformedValue: newValue };
value.slice(selectionStart - wrapper.length, selectionStart) === });
wrapper && },
value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper [transformationWrapper]
) { );
unwrap(wrapper);
} else {
wrap(wrapper, properties, addInnerNewLines);
}
}
}
function unwrap(wrapper: string) { const insert = useCallback(
transformationWrapper((value, selectionStart, selectionEnd) => { (prepend: string) => {
let newValue = ""; transformationWrapper((value, selectionStart) => {
newValue += value.slice(0, selectionStart - wrapper.length); let newValue = "";
newValue += value.slice(selectionStart, selectionEnd); newValue += value.slice(0, selectionStart);
newValue += value.slice(wrapper.length + selectionEnd); newValue += prepend;
return { prependLength: -wrapper.length, transformedValue: newValue }; newValue += value.slice(selectionStart);
});
}
function preline(prepend: string) { return { prependLength: prepend.length, transformedValue: newValue };
transformationWrapper((value, selectionStart) => { });
const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1; },
[transformationWrapper]
);
let newValue = ""; const appendDoc = useCallback(
newValue += value.slice(0, lastNewLine); (append: string) => {
newValue += prepend; transformationWrapper((value) => {
newValue += value.slice(lastNewLine); 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) { let paste = event.clipboardData.getData("text/html");
transformationWrapper((value, selectionStart) => { paste = paste.replace(/<!--.*?-->/u, "");
let newValue = ""; paste = turndownService.turndown(paste);
newValue += value.slice(0, selectionStart); paste = paste.replace(/<!--.*?-->/u, "");
newValue += prepend;
newValue += value.slice(selectionStart);
return { prependLength: prepend.length, transformedValue: newValue }; const target = event.target as HTMLTextAreaElement;
}); target.value = paste;
} target.select();
event.preventDefault();
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 &ldquo;x&rdquo;</p>
</>
}
>
<Button
onClick={() => {
insert("[^x]");
appendDoc("\n\n[^x]: This is a footnote.");
}} }}
icon={Icon.Superscript} className="h-[50vh] w-[50vw] mobile:w-[75vw]"
/> />
</ToolTip> </Popup>
<ToolTip <div className="mb-4 flex flex-row gap-2">
placement="bottom" <ToolTip
content={ 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"> <div className="grid gap-2">
<h3 className="text-lg">Headers</h3>
<Button onClick={() => preline("# ")} text={"H1"} />
<Button onClick={() => preline("## ")} text={"H2"} />
<Button onClick={() => preline("### ")} text={"H3"} />
<Button onClick={() => preline("#### ")} text={"H4"} />
<Button onClick={() => preline("##### ")} text={"H5"} />
<Button onClick={() => preline("###### ")} text={"H6"} />
</div>
}
>
<Button icon={Icon.Title} />
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Toggle Bold</h3>}
>
<Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} />
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Toggle Italic</h3>}
>
<Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} />
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Toggle Inline Code</h3>
<p>
Makes the text monospace (like text from a computer terminal).
Usually used for stylistic purposes in transcripts.
</p>
</>
}
>
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Insert footnote</h3>
<p>When inserted &ldquo;x&rdquo;</p>
</>
}
>
<Button
onClick={() => {
insert("[^x]");
appendDoc("\n\n[^x]: This is a footnote.");
}}
icon={Icon.Superscript}
/>
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Transcripts</h3>
<p>
Use this to create dialogues and transcripts. Start by adding
a container, then add transcript speech line within.
</p>
<div className="grid gap-2">
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Transcript container</h3>
</>
}
>
<Button
onClick={() => wrap("Transcript", {}, true)}
icon={Icon.AddBox}
/>
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Transcript speech line</h3>
<p>
Use to add a dialogue/transcript line. Change the{" "}
<kbd>name</kbd> property to chang the name of the
speaker
</p>
</>
}
>
<Button
onClick={() => wrap("Line", { name: "speaker" })}
icon={Icon.RecordVoiceOver}
/>
</ToolTip>
</div>
</>
}
>
<Button icon={Icon.RecordVoiceOver} />
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Inset box</h3>}
>
<Button
onClick={() => wrap("InsetBox", {}, true)}
icon={Icon.CheckBoxOutlineBlank}
/>
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Scene break</h3>}
>
<Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} />
</ToolTip>
<ToolTip
content={
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-lg">Links</h3>
<ToolTip <ToolTip
placement="right" placement="right"
content={ content={
<> <>
<h3 className="text-lg">Transcript container</h3> <h3 className="text-lg">External Link</h3>
</> <p className="text-xs">
} Provides a link to another webpage / website
>
<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> </p>
</> </>
} }
> >
<Button <Button
onClick={() => wrap("Line", { name: "speaker" })} onClick={() => insert("[Link name](https://domain.com)")}
icon={Icon.RecordVoiceOver} icon={Icon.Link}
text={"External"}
/>
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Intralink</h3>
<p className="text-xs">
Interlinks are used to add links to a header within the
same document
</p>
</>
}
>
<Button
onClick={() => wrap("IntraLink", {})}
icon={Icon.Link}
text={"Internal"}
/>
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Intralink (with target)</h3>{" "}
<p className="text-xs">
Use this one if you want the intralink text to be
different from the target header&rsquo;s name.
</p>
</>
}
>
<Button
onClick={() => wrap("IntraLink", { target: "target" })}
icon={Icon.Link}
text="Internal (w/ target)"
/> />
</ToolTip> </ToolTip>
</div> </div>
</> }
} >
> <Button icon={Icon.Link} />
<Button icon={Icon.RecordVoiceOver} /> </ToolTip>
</ToolTip>
<ToolTip <ToolTip
placement="bottom" placement="bottom"
content={<h3 className="text-lg">Inset box</h3>} content={
> <h3 className="text-lg">Player&rsquo;s name placeholder</h3>
<Button }
onClick={() => wrap("InsetBox", {}, true)} >
icon={Icon.CheckBoxOutlineBlank} <Button onClick={() => insert("<player>")} icon={Icon.Person} />
/> </ToolTip>
</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 <ToolTip
placement="right" placement="bottom"
content={ content={<h3 className="text-lg">Open HTML Converter</h3>}
<> >
<h3 className="text-lg">Intralink</h3> <Button
<p className="text-xs"> onClick={() => {
Interlinks are used to add links to a header within the setConverterOpened(true);
same document }}
</p> icon={Icon.Html}
</> />
} </ToolTip>
>
<Button
onClick={() => wrap("IntraLink", {})}
icon={Icon.Link}
text={"Internal"}
/>
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Intralink (with target)</h3>{" "}
<p className="text-xs">
Use this one if you want the intralink text to be
different from the target header&rsquo;s name.
</p>
</>
}
>
<Button
onClick={() => wrap("IntraLink", { target: "target" })}
icon={Icon.Link}
text="Internal (w/ target)"
/>
</ToolTip>
</div>
}
>
<Button icon={Icon.Link} />
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Player&rsquo;s name placeholder</h3>}
>
<Button onClick={() => insert("<player>")} icon={Icon.Person} />
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Open HTML Converter</h3>}
>
<Button
onClick={() => {
setConverterOpened(true);
}}
icon={Icon.Html}
/>
</ToolTip>
</div>
<div className="grid grid-cols-2 gap-8">
<div>
<h2>Editor</h2>
<textarea
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>
<div>
<h2>Preview</h2> <div className="grid grid-cols-2 gap-8">
<div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8"> <div>
<Markdawn className="w-full" text={markdown} /> <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>
</div>
<div className="mt-8"> <div className="mt-8">
<TOC text={markdown} /> <TOC text={markdown} />
</div> </div>
</ContentPanel> </ContentPanel>
),
[
appendDoc,
converterOpened,
handleInput,
insert,
markdown,
preline,
toggleWrap,
wrap,
]
); );
return ( return (
<AppLayout <AppLayout
navTitle="Markdawn Editor" navTitle="Markdawn Editor"

View File

@ -49,15 +49,17 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { Fragment, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { isUntangibleGroupItem } from "helpers/libraryItem"; import { isUntangibleGroupItem } from "helpers/libraryItem";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
item: NonNullable< item: NonNullable<
GetLibraryItemQuery["libraryItems"] NonNullable<
>["data"][number]["attributes"]; GetLibraryItemQuery["libraryItems"]
>["data"][number]["attributes"]
>;
itemId: NonNullable< itemId: NonNullable<
GetLibraryItemQuery["libraryItems"] GetLibraryItemQuery["libraryItems"]
>["data"][number]["id"]; >["data"][number]["id"];
@ -67,448 +69,474 @@ export default function LibrarySlug(props: Props): JSX.Element {
const { item, itemId, langui, currencies } = props; const { item, itemId, langui, currencies } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
const hoverable = useMediaHoverable(); 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 [openLightBox, LightBox] = useLightBox();
const [keepInfoVisible, setKeepInfoVisible] = useState(false); const [keepInfoVisible, setKeepInfoVisible] = useState(false);
let displayOpenScans = false; useScrollTopOnChange(AnchorIds.ContentPanel, [item]);
if (item?.contents?.data)
for (const content of item.contents.data) {
if (
content.attributes?.scan_set &&
content.attributes.scan_set.length > 0
)
displayOpenScans = true;
}
const subPanel = ( const isVariantSet = useMemo(
<SubPanel> () =>
<ReturnButton item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
href="/library/" item.metadata[0].subtype?.data?.attributes?.slug === "variant-set",
title={langui.library} [item.metadata]
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 contentPanel = ( const displayOpenScans = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () =>
<LightBox /> item.contents?.data.some(
(content) =>
content.attributes?.scan_set && content.attributes.scan_set.length > 0
),
[item.contents?.data]
);
<ReturnButton const subPanel = useMemo(
href="/library/" () => (
title={langui.library} <SubPanel>
langui={langui} <ReturnButton
displayOn={ReturnButtonType.Mobile} href="/library/"
className="mb-10" title={langui.library}
/> langui={langui}
<div className="grid place-items-center gap-12"> displayOn={ReturnButtonType.Desktop}
<div horizontalLine
className="relative h-[50vh] w-full />
cursor-pointer drop-shadow-shade-xl desktop:mb-16 mobile:h-[60vh]"
onClick={() => { <div className="grid gap-4">
if (item?.thumbnail?.data?.attributes) { <NavOption title={langui.summary} url="#summary" border />
openLightBox([
getAssetURL( {item.gallery && item.gallery.data.length > 0 && (
item.thumbnail.data.attributes.url, <NavOption title={langui.gallery} url="#gallery" border />
ImageQuality.Large )}
),
]); <NavOption title={langui.details} url="#details" border />
}
}} {item.subitems && item.subitems.data.length > 0 && (
> <NavOption
{item?.thumbnail?.data?.attributes ? ( title={isVariantSet ? langui.variants : langui.subitems}
<Img url={isVariantSet ? "#variants" : "#subitems"}
image={item.thumbnail.data.attributes} border
quality={ImageQuality.Large}
className="h-full w-full object-contain"
/> />
) : ( )}
<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> </div>
</SubPanel>
),
[isVariantSet, item.contents, item.gallery, item.subitems, langui]
);
<InsetBox id="summary" className="grid place-items-center"> const contentPanel = useMemo(
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> () => (
{item?.subitem_of?.data[0]?.attributes && ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<div className="grid place-items-center"> <LightBox />
<p>{langui.subitem_of}</p>
<Button <ReturnButton
href={`/library/${item.subitem_of.data[0].attributes.slug}`} href="/library/"
text={prettyinlineTitle( title={langui.library}
"", langui={langui}
item.subitem_of.data[0].attributes.title, displayOn={ReturnButtonType.Mobile}
item.subitem_of.data[0].attributes.subtitle className="mb-10"
)} />
/> <div className="grid place-items-center gap-12">
</div> <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"> </div>
<h1 className="text-3xl">{item?.title}</h1>
{item && isDefinedAndNotEmpty(item.subtitle) && ( <InsetBox id="summary" className="grid place-items-center">
<h2 className="text-2xl">{item.subtitle}</h2> <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> <div className="grid place-items-center text-center">
<h1 className="text-3xl">{item.title}</h1>
<PreviewCardCTAs {isDefinedAndNotEmpty(item.subtitle) && (
id={itemId} <h2 className="text-2xl">{item.subtitle}</h2>
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>
</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> </div>
)}
{item?.size && ( <PreviewCardCTAs
<div className="grid gap-8 mobile:place-items-center"> id={itemId}
<h3 className="text-xl">{langui.size}</h3> displayCTAs={!isUntangibleGroupItem(item.metadata?.[0])}
<div langui={langui}
className="grid w-full grid-flow-col place-content-between thin:grid-flow-row expand
thin:place-content-center thin:gap-8" />
> {item.descriptions?.[0] && (
<div <p className="text-justify">
className="grid place-items-center gap-x-4 desktop:grid-flow-col {item.descriptions[0].description}
desktop:place-items-start" </p>
> )}
<p className="font-bold">{langui.width}:</p> {!(
<div> item.metadata &&
<p>{item.size.width} mm</p> item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
<p>{convertMmToInch(item.size.width)} in</p> (item.metadata[0].subtype?.data?.attributes?.slug ===
</div> "variant-set" ||
</div> item.metadata[0].subtype?.data?.attributes?.slug ===
<div "relation-set")
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> {item.urls?.length ? (
<div className="grid w-full grid-cols-2 place-content-between"> <div className="flex flex-row place-items-center gap-3">
{item?.metadata?.[0]?.__typename === <p>{langui.available_at}</p>
"ComponentMetadataBooks" && ( {filterHasAttributes(item.urls).map((url, index) => (
<> <Fragment key={index}>
<div className="flex flex-row place-content-start gap-4"> <Button
<p className="font-bold">{langui.pages}:</p> href={url.url}
<p>{item.metadata[0].page_count}</p> target={"_blank"}
</div> text={prettyURL(url.url)}
/>
<div className="flex flex-row place-content-start gap-4"> </Fragment>
<p className="font-bold">{langui.binding}:</p> ))}
<p> </div>
{item.metadata[0].binding_type === ) : (
Enum_Componentmetadatabooks_Binding_Type.Paperback <p>{langui.item_not_available}</p>
? langui.paperback )}
: item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Hardcover
? langui.hardcover
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.page_order}:</p>
<p>
{item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.LeftToRight
? langui.left_to_right
: langui.right_to_left}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.languages}:</p>
{item.metadata[0]?.languages?.data.map((lang) => (
<p key={lang.attributes?.code}>
{lang.attributes?.name}
</p>
))}
</div>
</>
)}
</div>
</> </>
)} )}
</div> </div>
</InsetBox> </InsetBox>
{item?.subitems && item.subitems.data.length > 0 && ( {item.gallery && item.gallery.data.length > 0 && (
<div <div id="gallery" className="grid w-full place-items-center gap-8">
id={isVariantSet ? "variants" : "subitems"} <h2 className="text-2xl">{langui.gallery}</h2>
className="grid w-full place-items-center gap-8" <div
> className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
<h2 className="text-2xl"> gap-8"
{isVariantSet ? langui.variants : langui.subitems} >
</h2> {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 && ( <InsetBox id="details" className="grid place-items-center">
<WithLabel <div className="place-items grid w-[clamp(0px,100%,42rem)] gap-8">
label={langui.always_show_info} <h2 className="text-center text-2xl">{langui.details}</h2>
input={ <div
<Switch className="grid place-items-center gap-y-8
setState={setKeepInfoVisible} desktop:grid-flow-col desktop:place-content-between"
state={keepInfoVisible} >
/> {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 {item.release_date && (
className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] <div className="grid place-content-start place-items-center">
items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1" <h3 className="text-xl">{langui.release_date}</h3>
> <p>{prettyDate(item.release_date)}</p>
{filterHasAttributes(item.subitems.data).map((subitem) => ( </div>
<Fragment key={subitem.id}> )}
<PreviewCard
href={`/library/${subitem.attributes.slug}`} {item.price && (
title={subitem.attributes.title} <div className="grid place-content-start place-items-center text-center">
subtitle={subitem.attributes.subtitle} <h3 className="text-xl">{langui.price}</h3>
thumbnail={subitem.attributes.thumbnail?.data?.attributes} <p>
thumbnailAspectRatio="21/29.7" {prettyPrice(
thumbnailRounded={false} item.price,
keepInfoVisible={keepInfoVisible} currencies,
topChips={ item.price.currency?.data?.attributes?.code
subitem.attributes.metadata && )}
subitem.attributes.metadata.length > 0 && </p>
subitem.attributes.metadata[0] {item.price.currency?.data?.attributes?.code !==
? [prettyItemSubType(subitem.attributes.metadata[0])] appLayout.currency && (
: [] <p>
} {prettyPrice(
bottomChips={subitem.attributes.categories?.data.map( item.price,
(category) => category.attributes?.short ?? "" currencies,
appLayout.currency
)}{" "}
<br />({langui.calculated?.toLowerCase()})
</p>
)} )}
metadata={{ </div>
currencies: currencies, )}
release_date: subitem.attributes.release_date, </div>
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 && ( {item.categories && item.categories.data.length > 0 && (
<div id="contents" className="grid w-full place-items-center gap-8"> <div className="flex flex-col place-items-center gap-2">
<h2 className="-mb-6 text-2xl">{langui.contents}</h2> <h3 className="text-xl">{langui.categories}</h3>
{displayOpenScans && ( <div className="flex flex-row flex-wrap place-content-center gap-2">
<Button {item.categories.data.map((category) => (
href={`/library/${item.slug}/scans`} <Chip key={category.id}>{category.attributes?.name}</Chip>
text={langui.view_scans} ))}
/> </div>
)} </div>
<div className="grid w-full gap-4"> )}
{item.contents.data.map((content) => (
<ContentLine {item.size && (
langui={langui} <div className="grid gap-8 mobile:place-items-center">
content={content} <h3 className="text-xl">{langui.size}</h3>
parentSlug={item.slug} <div
key={content.id} 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> </InsetBox>
)}
</div> {item.subitems && item.subitems.data.length > 0 && (
</ContentPanel> <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 ( return (
<AppLayout <AppLayout
navTitle={prettyinlineTitle("", item?.title, item?.subtitle)} navTitle={prettyinlineTitle("", item.title, item.subtitle)}
contentPanel={contentPanel} contentPanel={contentPanel}
subPanel={subPanel} subPanel={subPanel}
thumbnail={item?.thumbnail?.data?.attributes ?? undefined} thumbnail={item.thumbnail?.data?.attributes ?? undefined}
description={item?.descriptions?.[0]?.description ?? undefined} description={item.descriptions?.[0]?.description ?? undefined}
{...props} {...props}
/> />
); );
@ -526,6 +554,7 @@ export async function getStaticProps(
language_code: context.locale ?? "en", language_code: context.locale ?? "en",
}); });
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true }; if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };
sortContent(item.libraryItems.data[0].attributes.contents);
const props: Props = { const props: Props = {
...(await getAppStaticProps(context)), ...(await getAppStaticProps(context)),
item: item.libraryItems.data[0].attributes, item: item.libraryItems.data[0].attributes,
@ -544,7 +573,7 @@ export async function getStaticPaths(
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(libraryItems.libraryItems?.data).map((item) => { filterHasAttributes(libraryItems.libraryItems?.data).map((item) => {
context.locales?.map((local) => context.locales?.map((local) =>
paths.push({ params: { slug: item.attributes?.slug }, locale: local }) paths.push({ params: { slug: item.attributes.slug }, locale: local })
); );
}); });

View File

@ -23,96 +23,112 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
item: NonNullable< item: NonNullable<
GetLibraryItemScansQuery["libraryItems"] NonNullable<
>["data"][number]["attributes"]; GetLibraryItemScansQuery["libraryItems"]
>["data"][number]["attributes"]
>;
itemId: NonNullable< itemId: NonNullable<
GetLibraryItemScansQuery["libraryItems"] NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["id"]
>["data"][number]["id"]; >;
} }
export default function LibrarySlug(props: Props): JSX.Element { export default function LibrarySlug(props: Props): JSX.Element {
const { item, langui, languages } = props; const { item, langui, languages } = props;
const [openLightBox, LightBox] = useLightBox(); const [openLightBox, LightBox] = useLightBox();
sortContent(item?.contents); sortContent(item.contents);
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href={`/library/${item?.slug}`} <ReturnButton
title={langui.item} href={`/library/${item.slug}`}
langui={langui} title={langui.item}
displayOn={ReturnButtonType.Desktop} langui={langui}
horizontalLine 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
/> />
))}
</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 = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
<LightBox /> <ContentPanel width={ContentPanelWidthSizes.Full}>
<LightBox />
<ReturnButton <ReturnButton
href={`/library/${item?.slug}`} href={`/library/${item.slug}`}
title={langui.item} title={langui.item}
langui={langui}
displayOn={ReturnButtonType.Mobile}
className="mb-10"
/>
{item?.images && (
<ScanSetCover
images={item.images}
openLightBox={openLightBox}
languages={languages}
langui={langui} langui={langui}
displayOn={ReturnButtonType.Mobile}
className="mb-10"
/> />
)}
{item?.contents?.data.map((content) => ( {item.images && (
<Fragment key={content.id}> <ScanSetCover
{content.attributes?.scan_set?.[0] && ( images={item.images}
<ScanSet openLightBox={openLightBox}
scanSet={content.attributes.scan_set} languages={languages}
openLightBox={openLightBox} langui={langui}
slug={content.attributes.slug} />
title={prettySlug(content.attributes.slug, item.slug)} )}
languages={languages}
langui={langui} {item.contents?.data.map((content) => (
content={content.attributes.content} <Fragment key={content.id}>
/> {content.attributes?.scan_set?.[0] && (
)} <ScanSet
</Fragment> scanSet={content.attributes.scan_set}
))} openLightBox={openLightBox}
</ContentPanel> 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 ( return (
<AppLayout <AppLayout
navTitle={prettyinlineTitle("", item?.title, item?.subtitle)} navTitle={prettyinlineTitle("", item.title, item.subtitle)}
contentPanel={contentPanel} contentPanel={contentPanel}
subPanel={subPanel} subPanel={subPanel}
thumbnail={item?.thumbnail?.data?.attributes ?? undefined} thumbnail={item.thumbnail?.data?.attributes ?? undefined}
{...props} {...props}
/> />
); );
@ -129,7 +145,8 @@ export async function getStaticProps(
: "", : "",
language_code: context.locale ?? "en", 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 = { const props: Props = {
...(await getAppStaticProps(context)), ...(await getAppStaticProps(context)),
item: item.libraryItems.data[0].attributes, item: item.libraryItems.data[0].attributes,

View File

@ -114,204 +114,226 @@ export default function Library(props: Props): JSX.Element {
[langui, groupingMethod, sortedItems] [langui, groupingMethod, sortedItems]
); );
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.LibraryBooks} <PanelHeader
title={langui.library} icon={Icon.LibraryBooks}
description={langui.library_description} title={langui.library}
/> description={langui.library_description}
/>
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} state={searchName}
setState={setSearchName} 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 <WithLabel
label={langui.always_show_info} label={langui.group_by}
input={ 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"> <WithLabel
<ToolTip content={langui.only_display_items_i_want}> label={langui.order_by}
<Button input={
icon={Icon.Favorite} <Select
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)} className="w-full"
active={filterUserStatus === LibraryItemUserStatus.Want} options={[
/> langui.name ?? "Name",
</ToolTip> langui.price ?? "Price",
<ToolTip content={langui.only_display_items_i_have}> langui.release_date ?? "Release date",
<Button ]}
icon={Icon.BackHand} state={sortingMethod}
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)} setState={setSortingMethod}
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 <WithLabel
className="mt-8" label={langui.show_subitems}
text={langui.reset_all_filters} input={<Switch state={showSubitems} setState={setShowSubitems} />}
icon={Icon.Replay} />
onClick={() => {
setSearchName(defaultFiltersState.searchName); <WithLabel
setShowSubitems(defaultFiltersState.showSubitems); label={langui.show_primary_items}
setShowPrimaryItems(defaultFiltersState.showPrimaryItems); input={
setShowSecondaryItems(defaultFiltersState.showSecondaryItems); <Switch state={showPrimaryItems} setState={setShowPrimaryItems} />
setSortingMethod(defaultFiltersState.sortingMethod); }
setGroupingMethod(defaultFiltersState.groupingMethod); />
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
setFilterUserStatus(defaultFiltersState.filterUserStatus); <WithLabel
}} label={langui.show_secondary_items}
/> input={
</SubPanel> <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}> const contentPanel = useMemo(
{/* TODO: Add to langui */} () => (
{groups.size === 0 && ( <ContentPanel width={ContentPanelWidthSizes.Full}>
<ContentPlaceholder {/* TODO: Add to langui */}
message={ {groups.size === 0 && (
"No results. You can try changing or resetting the search parameters." <ContentPlaceholder
} message={
icon={Icon.ChevronLeft} "No results. You can try changing or resetting the search parameters."
/> }
)} icon={Icon.ChevronLeft}
{iterateMap(groups, (name, items) => ( />
<Fragment key={name}> )}
{isDefinedAndNotEmpty(name) && ( {iterateMap(groups, (name, items) => (
<h2 <Fragment key={name}>
className="flex flex-row place-items-center gap-2 {isDefinedAndNotEmpty(name) && (
<h2
className="flex flex-row place-items-center gap-2
pb-2 pt-10 text-2xl first-of-type:pt-0" pb-2 pt-10 text-2xl first-of-type:pt-0"
> >
{name} {name}
<Chip>{`${items.length} ${ <Chip>{`${items.length} ${
items.length <= 1 items.length <= 1
? langui.result?.toLowerCase() ?? "result" ? langui.result?.toLowerCase() ?? "result"
: langui.results?.toLowerCase() ?? "results" : langui.results?.toLowerCase() ?? "results"
}`}</Chip> }`}</Chip>
</h2> </h2>
)} )}
<div <div
className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 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))] last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]
mobile:grid-cols-2 mobile:gap-4" mobile:grid-cols-2 mobile:gap-4"
> >
{filterHasAttributes(items).map((item) => ( {filterHasAttributes(items).map((item) => (
<Fragment key={item.id}> <Fragment key={item.id}>
<PreviewCard <PreviewCard
href={`/library/${item.attributes.slug}`} href={`/library/${item.attributes.slug}`}
title={item.attributes.title} title={item.attributes.title}
subtitle={item.attributes.subtitle} subtitle={item.attributes.subtitle}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7" thumbnailAspectRatio="21/29.7"
thumbnailRounded={false} thumbnailRounded={false}
keepInfoVisible={keepInfoVisible} keepInfoVisible={keepInfoVisible}
topChips={ topChips={
item.attributes.metadata && item.attributes.metadata &&
item.attributes.metadata.length > 0 && item.attributes.metadata.length > 0 &&
item.attributes.metadata[0] item.attributes.metadata[0]
? [prettyItemSubType(item.attributes.metadata[0])] ? [prettyItemSubType(item.attributes.metadata[0])]
: [] : []
} }
bottomChips={item.attributes.categories?.data.map( bottomChips={item.attributes.categories?.data.map(
(category) => category.attributes?.short ?? "" (category) => category.attributes?.short ?? ""
)} )}
metadata={{ metadata={{
currencies: currencies, currencies: currencies,
release_date: item.attributes.release_date, release_date: item.attributes.release_date,
price: item.attributes.price, price: item.attributes.price,
position: "Bottom", position: "Bottom",
}} }}
infoAppend={ infoAppend={
<PreviewCardCTAs <PreviewCardCTAs
id={item.id} id={item.id}
displayCTAs={ displayCTAs={
!isUntangibleGroupItem(item.attributes.metadata?.[0]) !isUntangibleGroupItem(item.attributes.metadata?.[0])
} }
langui={langui} langui={langui}
/> />
} }
/> />
</Fragment> </Fragment>
))} ))}
</div> </div>
</Fragment> </Fragment>
))} ))}
</ContentPanel> </ContentPanel>
),
[currencies, groups, keepInfoVisible, langui]
); );
return ( return (
<AppLayout <AppLayout
navTitle={langui.library} navTitle={langui.library}

View File

@ -5,18 +5,22 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { useMemo } from "react";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Merch(props: Props): JSX.Element { export default function Merch(props: Props): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.Store} <PanelHeader
title={langui.merch} icon={Icon.Store}
description={langui.merch_description} title={langui.merch}
/> description={langui.merch_description}
</SubPanel> />
</SubPanel>
),
[langui]
); );
return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />; return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />;

View File

@ -46,73 +46,79 @@ export default function News(props: Props): JSX.Element {
[posts, searchName] [posts, searchName]
); );
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.Feed} <PanelHeader
title={langui.news} icon={Icon.Feed}
description={langui.news_description} 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} />
}
/> />
)}
<Button <TextInput
className="mt-8" className="mb-6 w-full"
text={langui.reset_all_filters} placeholder={langui.search_title ?? undefined}
icon={Icon.Replay} state={searchName}
onClick={() => { setState={setSearchName}
setSearchName(defaultFiltersState.searchName); />
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
}} {hoverable && (
/> <WithLabel
</SubPanel> 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 = ( const contentPanel = useMemo(
<ContentPanel width={ContentPanelWidthSizes.Full}> () => (
<div <ContentPanel width={ContentPanelWidthSizes.Full}>
className="grid grid-cols-1 items-end gap-8 <div
className="grid grid-cols-1 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]" desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"
> >
{filterHasAttributes(filteredItems).map((post) => ( {filterHasAttributes(filteredItems).map((post) => (
<Fragment key={post.id}> <Fragment key={post.id}>
<PreviewCard <PreviewCard
href={`/news/${post.attributes.slug}`} href={`/news/${post.attributes.slug}`}
title={ title={
post.attributes.translations?.[0]?.title ?? post.attributes.translations?.[0]?.title ??
prettySlug(post.attributes.slug) prettySlug(post.attributes.slug)
} }
description={post.attributes.translations?.[0]?.excerpt} description={post.attributes.translations?.[0]?.excerpt}
thumbnail={post.attributes.thumbnail?.data?.attributes} thumbnail={post.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2" thumbnailAspectRatio="3/2"
thumbnailForceAspectRatio thumbnailForceAspectRatio
bottomChips={post.attributes.categories?.data.map( bottomChips={post.attributes.categories?.data.map(
(category) => category.attributes?.short ?? "" (category) => category.attributes?.short ?? ""
)} )}
keepInfoVisible={keepInfoVisible} keepInfoVisible={keepInfoVisible}
metadata={{ metadata={{
release_date: post.attributes.date, release_date: post.attributes.date,
position: "Top", position: "Top",
}} }}
/> />
</Fragment> </Fragment>
))} ))}
</div> </div>
</ContentPanel> </ContentPanel>
),
[filteredItems, keepInfoVisible]
); );
return ( return (

View File

@ -26,6 +26,7 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
page: WikiPageWithTranslations; page: WikiPageWithTranslations;
@ -40,80 +41,95 @@ export default function WikiPage(props: Props): JSX.Element {
languageExtractor: (item) => item.language?.data?.attributes?.code, languageExtractor: (item) => item.language?.data?.attributes?.code,
}); });
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href={`/wiki`} <ReturnButton
title={langui.wiki} href={`/wiki`}
langui={langui} title={langui.wiki}
displayOn={ReturnButtonType.Desktop} langui={langui}
horizontalLine displayOn={ReturnButtonType.Desktop}
/> horizontalLine
</SubPanel> />
</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"> const contentPanel = useMemo(
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1> () => (
<LanguageSwitcher /> <ContentPanel width={ContentPanelWidthSizes.Large}>
</div> <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 && ( <HorizontalLine />
<div className="text-justify">
<div {selectedTranslation && (
className="float-right ml-8 mb-8 w-[25rem] overflow-hidden rounded-lg bg-mid <div className="text-justify">
<div
className="float-right ml-8 mb-8 w-[25rem] overflow-hidden rounded-lg bg-mid
text-center" text-center"
> >
{page.thumbnail?.data?.attributes && ( {page.thumbnail?.data?.attributes && (
<Img image={page.thumbnail.data.attributes} /> <Img image={page.thumbnail.data.attributes} />
)} )}
<div className="my-4 grid gap-4 p-4"> <div className="my-4 grid gap-4 p-4">
<p className="font-headers text-xl">{langui.categories}</p> <p className="font-headers text-xl">{langui.categories}</p>
<div className="flex flex-row flex-wrap place-content-center gap-2"> <div className="flex flex-row flex-wrap place-content-center gap-2">
{page.categories?.data.map((category) => ( {page.categories?.data.map((category) => (
<Chip key={category.id}>{category.attributes?.name}</Chip> <Chip key={category.id}>{category.attributes?.name}</Chip>
))} ))}
</div>
</div> </div>
</div> </div>
</div> {isDefinedAndNotEmpty(selectedTranslation.summary) && (
{isDefinedAndNotEmpty(selectedTranslation.summary) && ( <div className="mb-6">
<div className="mb-6"> <p className="font-headers text-lg">{langui.summary}</p>
<p className="font-headers text-lg">{langui.summary}</p> <p>{selectedTranslation.summary}</p>
<p>{selectedTranslation.summary}</p> </div>
</div> )}
)}
{filterHasAttributes(page.definitions, ["translations"]).map( {filterHasAttributes(page.definitions, ["translations"]).map(
(definition, index) => ( (definition, index) => (
<DefinitionCard <DefinitionCard
key={index} key={index}
source={definition.source?.data?.attributes?.name} source={definition.source?.data?.attributes?.name}
translations={filterHasAttributes(definition.translations).map( translations={filterHasAttributes(
(translation) => ({ definition.translations
).map((translation) => ({
language: translation.language.data?.attributes?.code, language: translation.language.data?.attributes?.code,
definition: translation.definition, definition: translation.definition,
status: translation.status, status: translation.status,
}) }))}
)} index={index + 1}
index={index + 1} languages={languages}
languages={languages} langui={langui}
langui={langui} />
/> )
) )}
)} </div>
</div> )}
)} </ContentPanel>
</ContentPanel> ),
[
LanguageSwitcher,
languages,
langui,
page.categories?.data,
page.definitions,
page.thumbnail?.data?.attributes,
selectedTranslation,
]
); );
return ( return (

View File

@ -14,7 +14,7 @@ import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
import { filterHasAttributes, isDefined } from "helpers/others"; import { filterHasAttributes, isDefined } from "helpers/others";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
chronologyItems: NonNullable< chronologyItems: NonNullable<
@ -27,109 +27,113 @@ export default function Chronology(props: Props): JSX.Element {
const { chronologyItems, chronologyEras, langui } = props; const { chronologyItems, chronologyEras, langui } = props;
// Group by year the Chronology items // 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(() => { let currentChronologyEraIndex = 0;
chronologyItemYearGroups.push([]); chronologyItems.map((item) => {
}); if (item.attributes) {
if (
let currentChronologyEraIndex = 0; item.attributes.year >
chronologyItems.map((item) => { (chronologyEras[currentChronologyEraIndex].attributes?.ending_year ??
if (item.attributes) { 999999)
if ( ) {
item.attributes.year > currentChronologyEraIndex += 1;
(chronologyEras[currentChronologyEraIndex].attributes?.ending_year ?? }
999999) if (
) { Object.prototype.hasOwnProperty.call(
currentChronologyEraIndex += 1; memo[currentChronologyEraIndex],
item.attributes.year
)
) {
memo[currentChronologyEraIndex][item.attributes.year].push(item);
} else {
memo[currentChronologyEraIndex][item.attributes.year] = [item];
}
} }
if ( });
Object.prototype.hasOwnProperty.call( return memo;
chronologyItemYearGroups[currentChronologyEraIndex], }, [chronologyEras, chronologyItems]);
item.attributes.year
)
) {
chronologyItemYearGroups[currentChronologyEraIndex][
item.attributes.year
].push(item);
} else {
chronologyItemYearGroups[currentChronologyEraIndex][
item.attributes.year
] = [item];
}
}
});
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<ReturnButton <SubPanel>
href="/wiki" <ReturnButton
title={langui.wiki} href="/wiki"
langui={langui} title={langui.wiki}
displayOn={ReturnButtonType.Desktop} langui={langui}
horizontalLine displayOn={ReturnButtonType.Desktop}
/> horizontalLine
/>
{filterHasAttributes(chronologyEras).map((era) => ( {filterHasAttributes(chronologyEras).map((era) => (
<Fragment key={era.id}> <Fragment key={era.id}>
<NavOption <NavOption
url={`#${era.attributes.slug}`} url={`#${era.attributes.slug}`}
title={ title={
era.attributes.title && era.attributes.title &&
era.attributes.title.length > 0 && era.attributes.title.length > 0 &&
era.attributes.title[0] era.attributes.title[0]
? era.attributes.title[0].title ? era.attributes.title[0].title
: prettySlug(era.attributes.slug) : prettySlug(era.attributes.slug)
} }
subtitle={`${era.attributes.starting_year}${era.attributes.ending_year}`} subtitle={`${era.attributes.starting_year}${era.attributes.ending_year}`}
border border
/> />
</Fragment> </Fragment>
))} ))}
</SubPanel> </SubPanel>
),
[chronologyEras, langui]
); );
const contentPanel = ( const contentPanel = useMemo(
<ContentPanel> () => (
<ReturnButton <ContentPanel>
href="/wiki" <ReturnButton
title={langui.wiki} href="/wiki"
langui={langui} title={langui.wiki}
displayOn={ReturnButtonType.Mobile} langui={langui}
className="mb-10" displayOn={ReturnButtonType.Mobile}
/> className="mb-10"
/>
{chronologyItemYearGroups.map((era, eraIndex) => ( {chronologyItemYearGroups.map((era, eraIndex) => (
<Fragment key={eraIndex}> <Fragment key={eraIndex}>
<InsetBox <InsetBox
id={chronologyEras[eraIndex].attributes?.slug} id={chronologyEras[eraIndex].attributes?.slug}
className="my-8 grid gap-4 text-center" className="my-8 grid gap-4 text-center"
> >
<h2 className="text-2xl"> <h2 className="text-2xl">
{chronologyEras[eraIndex].attributes?.title?.[0] {chronologyEras[eraIndex].attributes?.title?.[0]
? chronologyEras[eraIndex].attributes?.title?.[0]?.title ? chronologyEras[eraIndex].attributes?.title?.[0]?.title
: prettySlug(chronologyEras[eraIndex].attributes?.slug)} : prettySlug(chronologyEras[eraIndex].attributes?.slug)}
</h2> </h2>
<p className="whitespace-pre-line "> <p className="whitespace-pre-line ">
{chronologyEras[eraIndex].attributes?.title?.[0] {chronologyEras[eraIndex].attributes?.title?.[0]
? chronologyEras[eraIndex].attributes?.title?.[0]?.description ? chronologyEras[eraIndex].attributes?.title?.[0]?.description
: ""} : ""}
</p> </p>
</InsetBox> </InsetBox>
{era.map((items, index) => ( {era.map((items, index) => (
<Fragment key={index}> <Fragment key={index}>
{items[0].attributes && isDefined(items[0].attributes.year) && ( {items[0].attributes && isDefined(items[0].attributes.year) && (
<ChronologyYearComponent <ChronologyYearComponent
year={items[0].attributes.year} year={items[0].attributes.year}
items={items} items={items}
langui={langui} langui={langui}
/> />
)} )}
</Fragment> </Fragment>
))} ))}
</Fragment> </Fragment>
))} ))}
</ContentPanel> </ContentPanel>
),
[chronologyEras, chronologyItemYearGroups, langui]
); );
return ( return (

View File

@ -47,89 +47,95 @@ export default function Wiki(props: Props): JSX.Element {
[pages, searchName] [pages, searchName]
); );
const subPanel = ( const subPanel = useMemo(
<SubPanel> () => (
<PanelHeader <SubPanel>
icon={Icon.TravelExplore} <PanelHeader
title={langui.wiki} icon={Icon.TravelExplore}
description={langui.wiki_description} 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} />
}
/> />
)}
<Button <TextInput
className="mt-8" className="mb-6 w-full"
text={langui.reset_all_filters} placeholder={langui.search_title ?? undefined}
icon={Icon.Replay} state={searchName}
onClick={() => { setState={setSearchName}
setSearchName(defaultFiltersState.searchName); />
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
}}
/>
<HorizontalLine />
{/* TODO: Langui */} {hoverable && (
<p className="mb-4 font-headers text-xl">Special Pages</p> <WithLabel
label={langui.always_show_info}
<NavOption title={langui.chronology} url="/wiki/chronology" border /> input={
</SubPanel> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
);
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."
} }
icon={Icon.ChevronLeft}
/> />
)} )}
{filterHasAttributes(filteredPages).map((page) => (
<Fragment key={page.id}> <Button
<TranslatedPreviewCard className="mt-8"
href={`/wiki/${page.attributes.slug}`} text={langui.reset_all_filters}
translations={page.attributes.translations?.map( icon={Icon.Replay}
(translation) => ({ onClick={() => {
title: translation?.title, setSearchName(defaultFiltersState.searchName);
description: translation?.summary, setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
language: translation?.language?.data?.attributes?.code, }}
}) />
)} <HorizontalLine />
thumbnail={page.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio={"4/3"} {/* TODO: Langui */}
thumbnailRounded <p className="mb-4 font-headers text-xl">Special Pages</p>
thumbnailForceAspectRatio
languages={languages} <NavOption title={langui.chronology} url="/wiki/chronology" border />
slug={page.attributes.slug} </SubPanel>
keepInfoVisible={keepInfoVisible} ),
bottomChips={page.attributes.categories?.data.map( [hoverable, keepInfoVisible, langui, searchName]
(category) => category.attributes?.short ?? "" );
)}
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> )}
))} {filterHasAttributes(filteredPages).map((page) => (
</div> <Fragment key={page.id}>
</ContentPanel> <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 ( return (