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

View File

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

View File

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

View File

@ -67,105 +67,136 @@ export function PostPage(props: Props): JSX.Element {
[post.slug, post.thumbnail, selectedTranslation]
);
const subPanel =
returnHref || returnTitle || displayCredits || displayToc ? (
<SubPanel>
const subPanel = useMemo(
() =>
returnHref || returnTitle || displayCredits || displayToc ? (
<SubPanel>
{returnHref && returnTitle && (
<ReturnButton
href={returnHref}
title={returnTitle}
langui={langui}
displayOn={ReturnButtonType.Desktop}
horizontalLine
/>
)}
{displayCredits && (
<>
{selectedTranslation && (
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.status,
langui
)}
maxWidth={"20rem"}
>
<Chip>{selectedTranslation.status}</Chip>
</ToolTip>
</div>
)}
{post.authors && post.authors.data.length > 0 && (
<div>
<p className="font-headers">{"Authors"}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(post.authors.data).map((author) => (
<Fragment key={author.id}>
<RecorderChip
langui={langui}
recorder={author.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
<HorizontalLine />
</>
)}
{displayToc && <TOC text={body} title={title} />}
</SubPanel>
) : undefined,
[
body,
displayCredits,
displayToc,
langui,
post.authors,
returnHref,
returnTitle,
selectedTranslation,
title,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel>
{returnHref && returnTitle && (
<ReturnButton
href={returnHref}
title={returnTitle}
langui={langui}
displayOn={ReturnButtonType.Desktop}
displayOn={ReturnButtonType.Mobile}
horizontalLine
/>
)}
{displayCredits && (
{displayThumbnailHeader ? (
<>
{selectedTranslation && (
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.status,
langui
)}
maxWidth={"20rem"}
>
<Chip>{selectedTranslation.status}</Chip>
</ToolTip>
</div>
)}
{post.authors && post.authors.data.length > 0 && (
<div>
<p className="font-headers">{"Authors"}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(post.authors.data).map((author) => (
<Fragment key={author.id}>
<RecorderChip
langui={langui}
recorder={author.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
<ThumbnailHeader
thumbnail={thumbnail}
title={title}
description={excerpt}
langui={langui}
categories={post.categories}
languageSwitcher={<LanguageSwitcher />}
/>
<HorizontalLine />
</>
) : (
<>
{displayLanguageSwitcher && (
<div className="grid place-content-end place-items-start">
<LanguageSwitcher />
</div>
)}
{displayTitle && (
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">
{title}
</h1>
)}
</>
)}
{displayToc && <TOC text={body} title={title} />}
</SubPanel>
) : undefined;
const contentPanel = (
<ContentPanel>
{returnHref && returnTitle && (
<ReturnButton
href={returnHref}
title={returnTitle}
langui={langui}
displayOn={ReturnButtonType.Mobile}
horizontalLine
/>
)}
{displayThumbnailHeader ? (
<>
<ThumbnailHeader
thumbnail={thumbnail}
title={title}
description={excerpt}
langui={langui}
categories={post.categories}
languageSwitcher={<LanguageSwitcher />}
/>
<HorizontalLine />
</>
) : (
<>
{displayLanguageSwitcher && (
<div className="grid place-content-end place-items-start">
<LanguageSwitcher />
</div>
)}
{displayTitle && (
<h1 className="my-16 flex justify-center gap-3 text-center text-4xl">
{title}
</h1>
)}
</>
)}
{prependBody}
<Markdawn text={body} />
{appendBody}
</ContentPanel>
{prependBody}
<Markdawn text={body} />
{appendBody}
</ContentPanel>
),
[
LanguageSwitcher,
appendBody,
body,
displayLanguageSwitcher,
displayThumbnailHeader,
displayTitle,
excerpt,
langui,
post.categories,
prependBody,
returnHref,
returnTitle,
thumbnail,
title,
]
);
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,321 +76,349 @@ export default function Content(props: Props): JSX.Element {
[content.group, content.slug]
);
const subPanel = (
<SubPanel>
<ReturnButton
href={`/contents`}
title={langui.contents}
langui={langui}
displayOn={ReturnButtonType.Desktop}
horizontalLine
/>
{selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && (
<div className="grid gap-5">
<h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes
.code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
</h2>
{selectedTranslation.text_set.source_language.data.attributes.code !==
selectedTranslation.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2">
<p className="font-headers">{langui.source_language}:</p>
<Chip>
{prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes
.code,
languages
)}
</Chip>
</div>
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.text_set.status,
langui
)}
maxWidth={"20rem"}
>
<Chip>{selectedTranslation.text_set.status}</Chip>
</ToolTip>
</div>
{selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.transcribers.data.length > 0 && (
<div>
<p className="font-headers">{langui.transcribers}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.transcribers.data
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.translators &&
selectedTranslation.text_set.translators.data.length > 0 && (
<div>
<p className="font-headers">{langui.translators}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.translators.data
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.proofreaders &&
selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div>
<p className="font-headers">{langui.proofreaders}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.proofreaders.data
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div>
<p className="font-headers">{"Notes"}:</p>
<div className="grid place-content-center place-items-center gap-2">
<Markdawn text={selectedTranslation.text_set.notes} />
</div>
</div>
)}
</div>
)}
{content.ranged_contents?.data &&
content.ranged_contents.data.length > 0 && (
<>
<HorizontalLine />
<div>
<p className="font-headers text-2xl">{langui.source}</p>
<div className="mt-6 grid place-items-center gap-6 text-left">
{content.ranged_contents.data.map((rangedContent) => {
const libraryItem =
rangedContent.attributes?.library_item?.data;
if (libraryItem?.attributes && libraryItem.id) {
return (
<div
key={libraryItem.attributes.slug}
className="mobile:w-[80%]"
>
<PreviewCard
href={`/library/${libraryItem.attributes.slug}`}
title={libraryItem.attributes.title}
subtitle={libraryItem.attributes.subtitle}
thumbnail={
libraryItem.attributes.thumbnail?.data?.attributes
}
thumbnailAspectRatio="21/29.7"
topChips={
libraryItem.attributes.metadata &&
libraryItem.attributes.metadata.length > 0 &&
libraryItem.attributes.metadata[0]
? [
prettyItemSubType(
libraryItem.attributes.metadata[0]
),
]
: []
}
bottomChips={libraryItem.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
metadata={{
currencies: currencies,
release_date: libraryItem.attributes.release_date,
price: libraryItem.attributes.price,
position: "Bottom",
}}
infoAppend={
<PreviewCardCTAs
id={libraryItem.id}
displayCTAs={
!isUntangibleGroupItem(
libraryItem.attributes.metadata?.[0]
)
}
langui={langui}
/>
}
/>
</div>
);
}
return <></>;
})}
</div>
</div>
</>
)}
{selectedTranslation?.text_set?.text && (
<>
<HorizontalLine />
<TOC
text={selectedTranslation.text_set.text}
title={prettyinlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)}
/>
</>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href={`/contents`}
title={langui.contents}
langui={langui}
displayOn={ReturnButtonType.Mobile}
className="mb-10"
/>
<div className="grid place-items-center">
<ThumbnailHeader
thumbnail={content.thumbnail?.data?.attributes}
pre_title={selectedTranslation?.pre_title}
title={selectedTranslation?.title}
subtitle={selectedTranslation?.subtitle}
description={selectedTranslation?.description}
type={content.type}
categories={content.categories}
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton
href={`/contents`}
title={langui.contents}
langui={langui}
languageSwitcher={<LanguageSwitcher />}
displayOn={ReturnButtonType.Desktop}
horizontalLine
/>
{previousContent?.attributes && (
<div className="mt-12 mb-8 w-full">
<h2 className="mb-4 text-center text-2xl">
{langui.previous_content}
{selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && (
<div className="grid gap-5">
<h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes
.code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
</h2>
<TranslatedPreviewLine
href={`/contents/${previousContent.attributes.slug}`}
translations={previousContent.attributes.translations?.map(
(translation) => ({
pre_title: translation?.pre_title,
title: translation?.title,
subtitle: translation?.subtitle,
language: translation?.language?.data?.attributes?.code,
})
{selectedTranslation.text_set.source_language.data.attributes
.code !==
selectedTranslation.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2">
<p className="font-headers">{langui.source_language}:</p>
<Chip>
{prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes
.code,
languages
)}
</Chip>
</div>
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.text_set.status,
langui
)}
maxWidth={"20rem"}
>
<Chip>{selectedTranslation.text_set.status}</Chip>
</ToolTip>
</div>
{selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.transcribers.data.length > 0 && (
<div>
<p className="font-headers">{langui.transcribers}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.transcribers.data
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
slug={previousContent.attributes.slug}
languages={languages}
thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
topChips={
isMobile
? undefined
: previousContent.attributes.type?.data?.attributes
? [
previousContent.attributes.type.data.attributes
.titles?.[0]
? previousContent.attributes.type.data.attributes
.titles[0]?.title
: prettySlug(
previousContent.attributes.type.data.attributes.slug
),
]
: undefined
}
bottomChips={
isMobile
? undefined
: previousContent.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
}
/>
{selectedTranslation.text_set.translators &&
selectedTranslation.text_set.translators.data.length > 0 && (
<div>
<p className="font-headers">{langui.translators}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.translators.data
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.proofreaders &&
selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div>
<p className="font-headers">{langui.proofreaders}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.proofreaders.data
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div>
<p className="font-headers">{"Notes"}:</p>
<div className="grid place-content-center place-items-center gap-2">
<Markdawn text={selectedTranslation.text_set.notes} />
</div>
</div>
)}
</div>
)}
<HorizontalLine />
{content.ranged_contents?.data &&
content.ranged_contents.data.length > 0 && (
<>
<HorizontalLine />
<div>
<p className="font-headers text-2xl">{langui.source}</p>
<div className="mt-6 grid place-items-center gap-6 text-left">
{content.ranged_contents.data.map((rangedContent) => {
const libraryItem =
rangedContent.attributes?.library_item?.data;
if (libraryItem?.attributes && libraryItem.id) {
return (
<div
key={libraryItem.attributes.slug}
className="mobile:w-[80%]"
>
<PreviewCard
href={`/library/${libraryItem.attributes.slug}`}
title={libraryItem.attributes.title}
subtitle={libraryItem.attributes.subtitle}
thumbnail={
libraryItem.attributes.thumbnail?.data?.attributes
}
thumbnailAspectRatio="21/29.7"
topChips={
libraryItem.attributes.metadata &&
libraryItem.attributes.metadata.length > 0 &&
libraryItem.attributes.metadata[0]
? [
prettyItemSubType(
libraryItem.attributes.metadata[0]
),
]
: []
}
bottomChips={libraryItem.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
metadata={{
currencies: currencies,
release_date: libraryItem.attributes.release_date,
price: libraryItem.attributes.price,
position: "Bottom",
}}
infoAppend={
<PreviewCardCTAs
id={libraryItem.id}
displayCTAs={
!isUntangibleGroupItem(
libraryItem.attributes.metadata?.[0]
)
}
langui={langui}
/>
}
/>
</div>
);
}
return <></>;
})}
</div>
</div>
</>
)}
<Markdawn text={selectedTranslation?.text_set?.text ?? ""} />
{nextContent?.attributes && (
{selectedTranslation?.text_set?.text && (
<>
<HorizontalLine />
<h2 className="mb-4 text-center text-2xl">
{langui.followup_content}
</h2>
<TranslatedPreviewLine
href={`/contents/${nextContent.attributes.slug}`}
translations={nextContent.attributes.translations?.map(
(translation) => ({
pre_title: translation?.pre_title,
title: translation?.title,
subtitle: translation?.subtitle,
language: translation?.language?.data?.attributes?.code,
})
<TOC
text={selectedTranslation.text_set.text}
title={prettyinlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)}
slug={nextContent.attributes.slug}
languages={languages}
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
topChips={
isMobile
? undefined
: nextContent.attributes.type?.data?.attributes
? [
nextContent.attributes.type.data.attributes.titles?.[0]
? nextContent.attributes.type.data.attributes.titles[0]
?.title
: prettySlug(
nextContent.attributes.type.data.attributes.slug
),
]
: undefined
}
bottomChips={
isMobile
? undefined
: nextContent.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
}
/>
</>
)}
</div>
</ContentPanel>
</SubPanel>
),
[
content.ranged_contents?.data,
currencies,
languages,
langui,
selectedTranslation,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel>
<ReturnButton
href={`/contents`}
title={langui.contents}
langui={langui}
displayOn={ReturnButtonType.Mobile}
className="mb-10"
/>
<div className="grid place-items-center">
<ThumbnailHeader
thumbnail={content.thumbnail?.data?.attributes}
pre_title={selectedTranslation?.pre_title}
title={selectedTranslation?.title}
subtitle={selectedTranslation?.subtitle}
description={selectedTranslation?.description}
type={content.type}
categories={content.categories}
langui={langui}
languageSwitcher={<LanguageSwitcher />}
/>
{previousContent?.attributes && (
<div className="mt-12 mb-8 w-full">
<h2 className="mb-4 text-center text-2xl">
{langui.previous_content}
</h2>
<TranslatedPreviewLine
href={`/contents/${previousContent.attributes.slug}`}
translations={previousContent.attributes.translations?.map(
(translation) => ({
pre_title: translation?.pre_title,
title: translation?.title,
subtitle: translation?.subtitle,
language: translation?.language?.data?.attributes?.code,
})
)}
slug={previousContent.attributes.slug}
languages={languages}
thumbnail={
previousContent.attributes.thumbnail?.data?.attributes
}
thumbnailAspectRatio="3/2"
topChips={
isMobile
? undefined
: previousContent.attributes.type?.data?.attributes
? [
previousContent.attributes.type.data.attributes
.titles?.[0]
? previousContent.attributes.type.data.attributes
.titles[0]?.title
: prettySlug(
previousContent.attributes.type.data.attributes
.slug
),
]
: undefined
}
bottomChips={
isMobile
? undefined
: previousContent.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
}
/>
</div>
)}
<HorizontalLine />
<Markdawn text={selectedTranslation?.text_set?.text ?? ""} />
{nextContent?.attributes && (
<>
<HorizontalLine />
<h2 className="mb-4 text-center text-2xl">
{langui.followup_content}
</h2>
<TranslatedPreviewLine
href={`/contents/${nextContent.attributes.slug}`}
translations={nextContent.attributes.translations?.map(
(translation) => ({
pre_title: translation?.pre_title,
title: translation?.title,
subtitle: translation?.subtitle,
language: translation?.language?.data?.attributes?.code,
})
)}
slug={nextContent.attributes.slug}
languages={languages}
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
topChips={
isMobile
? undefined
: nextContent.attributes.type?.data?.attributes
? [
nextContent.attributes.type.data.attributes.titles?.[0]
? nextContent.attributes.type.data.attributes
.titles[0]?.title
: prettySlug(
nextContent.attributes.type.data.attributes.slug
),
]
: undefined
}
bottomChips={
isMobile
? undefined
: nextContent.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
}
/>
</>
)}
</div>
</ContentPanel>
),
[
LanguageSwitcher,
content.categories,
content.thumbnail?.data?.attributes,
content.type,
isMobile,
languages,
langui,
nextContent?.attributes,
previousContent?.attributes,
selectedTranslation,
]
);
return (

View File

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

View File

@ -12,6 +12,7 @@ import { getReadySdk } from "graphql/sdk";
import { filterDefined, filterHasAttributes } from "helpers/others";
import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps {
contents: DevGetContentsQuery;
@ -21,61 +22,65 @@ export default function CheckupContents(props: Props): JSX.Element {
const { contents } = props;
const testReport = testingContent(contents);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.Full}>
{<h2 className="text-2xl">{testReport.title}</h2>}
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
{<h2 className="text-2xl">{testReport.title}</h2>}
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p></p>
<p></p>
<p className="font-headers">Ref</p>
<p className="font-headers">Name</p>
<p className="font-headers">Type</p>
<p className="font-headers">Severity</p>
<p className="font-headers">Description</p>
</div>
{testReport.lines.map((line, index) => (
<div
key={index}
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
justify-items-start gap-2"
>
<Button
href={line.frontendUrl}
target="_blank"
className="w-4 text-xs"
text="F"
/>
<Button
href={line.backendUrl}
target="_blank"
className="w-4 text-xs"
text="B"
/>
<p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p>
<Chip>{line.type}</Chip>
<Chip
className={
line.severity === "Very High"
? "bg-[#f00] font-bold !opacity-100"
: line.severity === "High"
? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === "Medium"
? "bg-[#fff344] !opacity-100"
: ""
}
>
{line.severity}
</Chip>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>
</ToolTip>
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p></p>
<p></p>
<p className="font-headers">Ref</p>
<p className="font-headers">Name</p>
<p className="font-headers">Type</p>
<p className="font-headers">Severity</p>
<p className="font-headers">Description</p>
</div>
))}
</ContentPanel>
{testReport.lines.map((line, index) => (
<div
key={index}
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
justify-items-start gap-2"
>
<Button
href={line.frontendUrl}
target="_blank"
className="w-4 text-xs"
text="F"
/>
<Button
href={line.backendUrl}
target="_blank"
className="w-4 text-xs"
text="B"
/>
<p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p>
<Chip>{line.type}</Chip>
<Chip
className={
line.severity === "Very High"
? "bg-[#f00] font-bold !opacity-100"
: line.severity === "High"
? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === "Medium"
? "bg-[#fff344] !opacity-100"
: ""
}
>
{line.severity}
</Chip>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>
</ToolTip>
</div>
))}
</ContentPanel>
),
[testReport.lines, testReport.title]
);
return (
<AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} />
);

View File

@ -14,6 +14,7 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next";
import { useMemo } from "react";
interface Props extends AppStaticProps {
libraryItems: DevGetLibraryItemsQuery;
@ -23,61 +24,65 @@ export default function CheckupLibraryItems(props: Props): JSX.Element {
const { libraryItems } = props;
const testReport = testingLibraryItem(libraryItems);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.Full}>
{<h2 className="text-2xl">{testReport.title}</h2>}
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
{<h2 className="text-2xl">{testReport.title}</h2>}
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p></p>
<p></p>
<p className="font-headers">Ref</p>
<p className="font-headers">Name</p>
<p className="font-headers">Type</p>
<p className="font-headers">Severity</p>
<p className="font-headers">Description</p>
</div>
{testReport.lines.map((line, index) => (
<div
key={index}
className="mb-2 grid
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2"
>
<Button
href={line.frontendUrl}
target="_blank"
className="w-4 text-xs"
text="F"
/>
<Button
href={line.backendUrl}
target="_blank"
className="w-4 text-xs"
text="B"
/>
<p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p>
<Chip>{line.type}</Chip>
<Chip
className={
line.severity === "Very High"
? "bg-[#f00] font-bold !opacity-100"
: line.severity === "High"
? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === "Medium"
? "bg-[#fff344] !opacity-100"
: ""
}
>
{line.severity}
</Chip>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>
</ToolTip>
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p></p>
<p></p>
<p className="font-headers">Ref</p>
<p className="font-headers">Name</p>
<p className="font-headers">Type</p>
<p className="font-headers">Severity</p>
<p className="font-headers">Description</p>
</div>
))}
</ContentPanel>
{testReport.lines.map((line, index) => (
<div
key={index}
className="mb-2 grid
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2"
>
<Button
href={line.frontendUrl}
target="_blank"
className="w-4 text-xs"
text="F"
/>
<Button
href={line.backendUrl}
target="_blank"
className="w-4 text-xs"
text="B"
/>
<p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p>
<Chip>{line.type}</Chip>
<Chip
className={
line.severity === "Very High"
? "bg-[#f00] font-bold !opacity-100"
: line.severity === "High"
? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === "Medium"
? "bg-[#fff344] !opacity-100"
: ""
}
>
{line.severity}
</Chip>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>
</ToolTip>
</div>
))}
</ContentPanel>
),
[testReport.lines, testReport.title]
);
return (
<AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} />
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,89 +47,95 @@ export default function Wiki(props: Props): JSX.Element {
[pages, searchName]
);
const subPanel = (
<SubPanel>
<PanelHeader
icon={Icon.TravelExplore}
title={langui.wiki}
description={langui.wiki_description}
/>
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? undefined}
state={searchName}
setState={setSearchName}
/>
{hoverable && (
<WithLabel
label={langui.always_show_info}
input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
}
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.TravelExplore}
title={langui.wiki}
description={langui.wiki_description}
/>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(defaultFiltersState.searchName);
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
}}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? undefined}
state={searchName}
setState={setSearchName}
/>
{/* TODO: Langui */}
<p className="mb-4 font-headers text-xl">Special Pages</p>
<NavOption title={langui.chronology} url="/wiki/chronology" border />
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<div
className="grid grid-cols-2 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4"
>
{/* TODO: Add to langui */}
{filteredPages.length === 0 && (
<ContentPlaceholder
message={
"No results. You can try changing or resetting the search parameters."
{hoverable && (
<WithLabel
label={langui.always_show_info}
input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
}
icon={Icon.ChevronLeft}
/>
)}
{filterHasAttributes(filteredPages).map((page) => (
<Fragment key={page.id}>
<TranslatedPreviewCard
href={`/wiki/${page.attributes.slug}`}
translations={page.attributes.translations?.map(
(translation) => ({
title: translation?.title,
description: translation?.summary,
language: translation?.language?.data?.attributes?.code,
})
)}
thumbnail={page.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio={"4/3"}
thumbnailRounded
thumbnailForceAspectRatio
languages={languages}
slug={page.attributes.slug}
keepInfoVisible={keepInfoVisible}
bottomChips={page.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(defaultFiltersState.searchName);
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
}}
/>
<HorizontalLine />
{/* TODO: Langui */}
<p className="mb-4 font-headers text-xl">Special Pages</p>
<NavOption title={langui.chronology} url="/wiki/chronology" border />
</SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<div
className="grid grid-cols-2 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4"
>
{/* TODO: Add to langui */}
{filteredPages.length === 0 && (
<ContentPlaceholder
message={
"No results. You can try changing or resetting the search parameters."
}
icon={Icon.ChevronLeft}
/>
</Fragment>
))}
</div>
</ContentPanel>
)}
{filterHasAttributes(filteredPages).map((page) => (
<Fragment key={page.id}>
<TranslatedPreviewCard
href={`/wiki/${page.attributes.slug}`}
translations={page.attributes.translations?.map(
(translation) => ({
title: translation?.title,
description: translation?.summary,
language: translation?.language?.data?.attributes?.code,
})
)}
thumbnail={page.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio={"4/3"}
thumbnailRounded
thumbnailForceAspectRatio
languages={languages}
slug={page.attributes.slug}
keepInfoVisible={keepInfoVisible}
bottomChips={page.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
/>
</Fragment>
))}
</div>
</ContentPanel>
),
[filteredPages, keepInfoVisible, languages]
);
return (