541 lines
20 KiB
TypeScript
541 lines
20 KiB
TypeScript
import AppLayout from "components/AppLayout";
|
||
import Chip from "components/Chip";
|
||
import Img from "components/Img";
|
||
import Button from "components/Inputs/Button";
|
||
import Switch from "components/Inputs/Switch";
|
||
import InsetBox from "components/InsetBox";
|
||
import ContentLine from "components/Library/ContentLine";
|
||
import LightBox from "components/LightBox";
|
||
import NavOption from "components/PanelComponents/NavOption";
|
||
import ReturnButton, {
|
||
ReturnButtonType,
|
||
} from "components/PanelComponents/ReturnButton";
|
||
import ContentPanel, {
|
||
ContentPanelWidthSizes,
|
||
} from "components/Panels/ContentPanel";
|
||
import SubPanel from "components/Panels/SubPanel";
|
||
import ThumbnailPreview from "components/PreviewCard";
|
||
import { useAppLayout } from "contexts/AppLayoutContext";
|
||
import {
|
||
Enum_Componentmetadatabooks_Binding_Type,
|
||
Enum_Componentmetadatabooks_Page_Order,
|
||
GetLibraryItemQuery,
|
||
} from "graphql/generated";
|
||
import { getReadySdk } from "graphql/sdk";
|
||
import {
|
||
prettyDate,
|
||
prettyinlineTitle,
|
||
prettyItemSubType,
|
||
prettyItemType,
|
||
prettyPrice,
|
||
prettyURL,
|
||
} from "helpers/formatters";
|
||
import { AppStaticProps, getAppStaticProps } from "helpers/getAppStaticProps";
|
||
import { sortContent } from "helpers/others";
|
||
import { getAssetURL, ImageQuality } from "helpers/img";
|
||
import { convertMmToInch } from "helpers/numbers";
|
||
import {
|
||
GetStaticPathsContext,
|
||
GetStaticPathsResult,
|
||
GetStaticPropsContext,
|
||
} from "next";
|
||
import { useState } from "react";
|
||
|
||
interface Props extends AppStaticProps {
|
||
item: Exclude<
|
||
GetLibraryItemQuery["libraryItems"],
|
||
null | undefined
|
||
>["data"][number]["attributes"];
|
||
itemId: Exclude<
|
||
GetLibraryItemQuery["libraryItems"],
|
||
null | undefined
|
||
>["data"][number]["id"];
|
||
}
|
||
|
||
export default function LibrarySlug(props: Props): JSX.Element {
|
||
const { item, langui, currencies } = props;
|
||
const appLayout = useAppLayout();
|
||
|
||
const isVariantSet =
|
||
item?.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
|
||
|
||
sortContent(item?.contents);
|
||
|
||
const [lightboxOpen, setLightboxOpen] = useState(false);
|
||
const [lightboxImages, setLightboxImages] = useState([""]);
|
||
const [lightboxIndex, setLightboxIndex] = useState(0);
|
||
|
||
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;
|
||
}
|
||
|
||
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
|
||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||
/>
|
||
|
||
{item?.gallery && item.gallery.data.length > 0 && (
|
||
<NavOption
|
||
title={langui.gallery}
|
||
url="#gallery"
|
||
border
|
||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||
/>
|
||
)}
|
||
|
||
<NavOption
|
||
title={langui.details}
|
||
url="#details"
|
||
border
|
||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||
/>
|
||
|
||
{item?.subitems && item.subitems.data.length > 0 && (
|
||
<NavOption
|
||
title={isVariantSet ? langui.variants : langui.subitems}
|
||
url={isVariantSet ? "#variants" : "#subitems"}
|
||
border
|
||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||
/>
|
||
)}
|
||
|
||
{item?.contents && item.contents.data.length > 0 && (
|
||
<NavOption
|
||
title={langui.contents}
|
||
url="#contents"
|
||
border
|
||
onClick={() => appLayout.setSubPanelOpen(false)}
|
||
/>
|
||
)}
|
||
</div>
|
||
</SubPanel>
|
||
);
|
||
|
||
const contentPanel = (
|
||
<ContentPanel width={ContentPanelWidthSizes.large}>
|
||
<LightBox
|
||
state={lightboxOpen}
|
||
setState={setLightboxOpen}
|
||
images={lightboxImages}
|
||
index={lightboxIndex}
|
||
setIndex={setLightboxIndex}
|
||
/>
|
||
|
||
<ReturnButton
|
||
href="/library/"
|
||
title={langui.library}
|
||
langui={langui}
|
||
displayOn={ReturnButtonType.mobile}
|
||
className="mb-10"
|
||
/>
|
||
<div className="grid place-items-center gap-12">
|
||
<div
|
||
className="drop-shadow-shade-xl w-full h-[50vh] mobile:h-[60vh] desktop:mb-16 relative cursor-pointer"
|
||
onClick={() => {
|
||
if (item?.thumbnail?.data?.attributes) {
|
||
setLightboxOpen(true);
|
||
setLightboxImages([
|
||
getAssetURL(
|
||
item.thumbnail.data.attributes.url,
|
||
ImageQuality.Large
|
||
),
|
||
]);
|
||
setLightboxIndex(0);
|
||
}
|
||
}}
|
||
>
|
||
{item?.thumbnail?.data?.attributes ? (
|
||
<Img
|
||
image={item.thumbnail.data.attributes}
|
||
quality={ImageQuality.Large}
|
||
layout="fill"
|
||
objectFit="contain"
|
||
priority
|
||
/>
|
||
) : (
|
||
<div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div>
|
||
)}
|
||
</div>
|
||
|
||
<InsetBox id="summary" className="grid place-items-center">
|
||
<div className="w-[clamp(0px,100%,42rem)] grid 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}`}
|
||
>
|
||
{prettyinlineTitle(
|
||
"",
|
||
item.subitem_of.data[0].attributes.title,
|
||
item.subitem_of.data[0].attributes.subtitle
|
||
)}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
<div className="grid place-items-center">
|
||
<h1 className="text-3xl">{item?.title}</h1>
|
||
{item?.subtitle && <h2 className="text-2xl">{item.subtitle}</h2>}
|
||
</div>
|
||
{item?.descriptions?.[0] && (
|
||
<p className="text-justify">{item.descriptions[0].description}</p>
|
||
)}
|
||
{!(
|
||
item?.metadata &&
|
||
item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
|
||
(item.metadata[0].subtype?.data?.attributes?.slug ===
|
||
"variant-set" ||
|
||
item.metadata[0].subtype?.data?.attributes?.slug ===
|
||
"relation-set")
|
||
) && (
|
||
<>
|
||
{item?.urls && item.urls.length ? (
|
||
<div className="flex flex-row place-items-center gap-3">
|
||
<p>Available at</p>
|
||
{item.urls.map((url) => (
|
||
<>
|
||
{url?.url && (
|
||
<Button
|
||
href={url.url}
|
||
key={url.url}
|
||
target={"_blank"}
|
||
>
|
||
{prettyURL(url.url)}
|
||
</Button>
|
||
)}
|
||
</>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p>This item is not for sale or is no longer available</p>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</InsetBox>
|
||
|
||
{item?.gallery && item.gallery.data.length > 0 && (
|
||
<div id="gallery" className="grid place-items-center gap-8 w-full">
|
||
<h2 className="text-2xl">{langui.gallery}</h2>
|
||
<div className="grid w-full gap-8 items-end grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]">
|
||
{item.gallery.data.map((galleryItem, index) => (
|
||
<>
|
||
{galleryItem.attributes && (
|
||
<div
|
||
key={galleryItem.id}
|
||
className="relative aspect-square hover:scale-[1.02] transition-transform cursor-pointer"
|
||
onClick={() => {
|
||
if (item.gallery?.data) {
|
||
const images: string[] = [];
|
||
item.gallery.data.map((image) => {
|
||
if (image.attributes)
|
||
images.push(
|
||
getAssetURL(
|
||
image.attributes.url,
|
||
ImageQuality.Large
|
||
)
|
||
);
|
||
});
|
||
setLightboxOpen(true);
|
||
setLightboxImages(images);
|
||
setLightboxIndex(index);
|
||
}
|
||
}}
|
||
>
|
||
<div className="bg-light absolute inset-0 rounded-lg drop-shadow-shade-md"></div>
|
||
<Img
|
||
className="rounded-lg"
|
||
image={galleryItem.attributes}
|
||
layout="fill"
|
||
objectFit="cover"
|
||
/>
|
||
</div>
|
||
)}
|
||
</>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<InsetBox id="details" className="grid place-items-center">
|
||
<div className="w-[clamp(0px,100%,42rem)] grid place-items gap-8">
|
||
<h2 className="text-2xl text-center">{langui.details}</h2>
|
||
<div className="grid grid-flow-col w-full place-content-between">
|
||
{item?.metadata?.[0] && (
|
||
<div className="grid place-items-center place-content-start">
|
||
<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-items-center place-content-start">
|
||
<h3 className="text-xl">{langui.release_date}</h3>
|
||
<p>{prettyDate(item.release_date)}</p>
|
||
</div>
|
||
)}
|
||
|
||
{item?.price && (
|
||
<div className="grid place-items-center text-center place-content-start">
|
||
<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 && (
|
||
<>
|
||
<h3 className="text-xl">{langui.size}</h3>
|
||
<div className="grid grid-flow-col w-full place-content-between">
|
||
<div className="flex flex-row flex-wrap place-items-start gap-4">
|
||
<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="flex flex-row flex-wrap place-items-start gap-4">
|
||
<p className="font-bold">{langui.height}:</p>
|
||
<div>
|
||
<p>{item.size.height} mm</p>
|
||
<p>{convertMmToInch(item.size.height)} in</p>
|
||
</div>
|
||
</div>
|
||
{item.size.thickness && (
|
||
<div className="flex flex-row flex-wrap place-items-start gap-4">
|
||
<p className="font-bold">{langui.thickness}:</p>
|
||
<div>
|
||
<p>{item.size.thickness} mm</p>
|
||
<p>{convertMmToInch(item.size.thickness)} in</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{item?.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
|
||
item?.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
|
||
<>
|
||
<h3 className="text-xl">{langui.type_information}</h3>
|
||
<div className="grid grid-cols-2 w-full 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>
|
||
</InsetBox>
|
||
|
||
{item?.subitems && item.subitems.data.length > 0 && (
|
||
<div
|
||
id={isVariantSet ? "variants" : "subitems"}
|
||
className="grid place-items-center gap-8 w-full"
|
||
>
|
||
<h2 className="text-2xl">
|
||
{isVariantSet ? langui.variants : langui.subitems}
|
||
</h2>
|
||
|
||
<div className="-mt-6 mb-8 flex flex-row gap-2 place-items-center coarse:hidden">
|
||
<p className="flex-shrink-0">{"Always show info"}:</p>
|
||
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
|
||
</div>
|
||
<div className="grid gap-8 items-end mobile:grid-cols-2 grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full">
|
||
{item.subitems.data.map((subitem) => (
|
||
<>
|
||
{subitem.attributes && (
|
||
<ThumbnailPreview
|
||
key={subitem.id}
|
||
href={`/library/${subitem.attributes.slug}`}
|
||
title={subitem.attributes.title}
|
||
subtitle={subitem.attributes.subtitle}
|
||
thumbnail={subitem.attributes.thumbnail?.data?.attributes}
|
||
thumbnailAspectRatio="21/29.7"
|
||
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",
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{item?.contents && item.contents.data.length > 0 && (
|
||
<div id="contents" className="w-full grid place-items-center gap-8">
|
||
<h2 className="text-2xl -mb-6">{langui.contents}</h2>
|
||
{displayOpenScans && (
|
||
<Button href={`/library/${item.slug}/scans`}>
|
||
{langui.view_scans}
|
||
</Button>
|
||
)}
|
||
<div className="grid gap-4 w-full">
|
||
{item.contents.data.map((content) => (
|
||
<ContentLine
|
||
langui={langui}
|
||
content={content}
|
||
parentSlug={item.slug}
|
||
key={content.id}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</ContentPanel>
|
||
);
|
||
|
||
return (
|
||
<AppLayout
|
||
navTitle={prettyinlineTitle("", item?.title, item?.subtitle)}
|
||
contentPanel={contentPanel}
|
||
subPanel={subPanel}
|
||
thumbnail={item?.thumbnail?.data?.attributes ?? undefined}
|
||
description={item?.descriptions?.[0]?.description ?? undefined}
|
||
{...props}
|
||
/>
|
||
);
|
||
}
|
||
|
||
export async function getStaticProps(
|
||
context: GetStaticPropsContext
|
||
): Promise<{ notFound: boolean } | { props: Props }> {
|
||
const sdk = getReadySdk();
|
||
const item = await sdk.getLibraryItem({
|
||
slug: context.params?.slug ? context.params.slug.toString() : "",
|
||
language_code: context.locale ?? "en",
|
||
});
|
||
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };
|
||
const props: Props = {
|
||
...(await getAppStaticProps(context)),
|
||
item: item.libraryItems.data[0].attributes,
|
||
itemId: item.libraryItems.data[0].id,
|
||
};
|
||
return {
|
||
props: props,
|
||
};
|
||
}
|
||
|
||
export async function getStaticPaths(
|
||
context: GetStaticPathsContext
|
||
): Promise<GetStaticPathsResult> {
|
||
const sdk = getReadySdk();
|
||
const libraryItems = await sdk.getLibraryItemsSlugs();
|
||
const paths: GetStaticPathsResult["paths"] = [];
|
||
if (libraryItems.libraryItems) {
|
||
libraryItems.libraryItems.data.map((item) => {
|
||
context.locales?.map((local) => {
|
||
paths.push({ params: { slug: item.attributes?.slug }, locale: local });
|
||
});
|
||
});
|
||
}
|
||
return {
|
||
paths,
|
||
fallback: "blocking",
|
||
};
|
||
}
|