272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { useRouter } from "next/router";
|
|
import { Chip } from "./Chip";
|
|
import { Ico, Icon } from "./Ico";
|
|
import { Img } from "./Img";
|
|
import { Link } from "./Inputs/Link";
|
|
import { DatePickerFragment, PricePickerFragment, UploadImageFragment } from "graphql/generated";
|
|
import { cIf, cJoin } from "helpers/className";
|
|
import { prettyDate, prettyDuration, prettyPrice, prettyShortenNumber } from "helpers/formatters";
|
|
import { ImageQuality } from "helpers/img";
|
|
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
|
import { TranslatedProps } from "types/TranslatedProps";
|
|
import { useUserSettings } from "contexts/UserSettingsContext";
|
|
import { useLocalData } from "contexts/LocalDataContext";
|
|
|
|
/*
|
|
* ╭─────────────╮
|
|
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
|
*/
|
|
|
|
interface Props {
|
|
thumbnail?: UploadImageFragment | string | null | undefined;
|
|
thumbnailAspectRatio?: string;
|
|
thumbnailForceAspectRatio?: boolean;
|
|
thumbnailRounded?: boolean;
|
|
href: string;
|
|
pre_title?: string | null | undefined;
|
|
title: string | null | undefined;
|
|
subtitle?: string | null | undefined;
|
|
description?: string | null | undefined;
|
|
topChips?: string[];
|
|
bottomChips?: string[];
|
|
keepInfoVisible?: boolean;
|
|
stackNumber?: number;
|
|
metadata?: {
|
|
releaseDate?: DatePickerFragment | null;
|
|
releaseDateFormat?: Intl.DateTimeFormatOptions["dateStyle"];
|
|
price?: PricePickerFragment | null;
|
|
views?: number;
|
|
author?: string;
|
|
position: "Bottom" | "Top";
|
|
};
|
|
infoAppend?: React.ReactNode;
|
|
hoverlay?:
|
|
| {
|
|
__typename: "Video";
|
|
duration: number;
|
|
}
|
|
| { __typename: "anotherHoverlayName" };
|
|
}
|
|
|
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
|
|
export const PreviewCard = ({
|
|
href,
|
|
thumbnail,
|
|
thumbnailAspectRatio = "4/3",
|
|
thumbnailForceAspectRatio = false,
|
|
thumbnailRounded = true,
|
|
pre_title,
|
|
title,
|
|
subtitle,
|
|
description,
|
|
stackNumber = 0,
|
|
topChips,
|
|
bottomChips,
|
|
keepInfoVisible,
|
|
metadata,
|
|
hoverlay,
|
|
infoAppend,
|
|
}: Props): JSX.Element => {
|
|
const { currency } = useUserSettings();
|
|
const { currencies } = useLocalData();
|
|
const isHoverable = useDeviceSupportsHover();
|
|
const router = useRouter();
|
|
|
|
const metadataJSX = useMemo(
|
|
() => (
|
|
<>
|
|
{metadata && (metadata.releaseDate || metadata.price) && (
|
|
<div className="flex w-full flex-row flex-wrap gap-x-3">
|
|
{metadata.releaseDate && (
|
|
<p className="text-sm">
|
|
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
|
|
{prettyDate(metadata.releaseDate, router.locale)}
|
|
</p>
|
|
)}
|
|
{metadata.price && (
|
|
<p className="justify-self-end text-sm">
|
|
<Ico icon={Icon.ShoppingCart} className="mr-1 translate-y-[.15em] !text-base" />
|
|
{prettyPrice(metadata.price, currencies, currency)}
|
|
</p>
|
|
)}
|
|
{metadata.views && (
|
|
<p className="text-sm">
|
|
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
|
|
{prettyShortenNumber(metadata.views)}
|
|
</p>
|
|
)}
|
|
{metadata.author && (
|
|
<p className="text-sm">
|
|
<Ico icon={Icon.Person} className="mr-1 translate-y-[.15em] !text-base" />
|
|
{metadata.author}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
),
|
|
[currencies, currency, metadata, router.locale]
|
|
);
|
|
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className="group grid cursor-pointer items-end text-left transition-transform
|
|
drop-shadow-shade-xl hover:scale-[1.02]">
|
|
{stackNumber > 0 && (
|
|
<>
|
|
<div
|
|
className={cJoin(
|
|
`absolute inset-0 scale-[.85] overflow-hidden bg-light brightness-[0.8] sepia-[0.5]
|
|
transition-[top_transform] group-hover:-top-9`,
|
|
cIf(thumbnailRounded, "rounded-md")
|
|
)}>
|
|
{thumbnail && (
|
|
<Img className="opacity-30" src={thumbnail} quality={ImageQuality.Medium} />
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={cJoin(
|
|
`absolute inset-0 overflow-hidden bg-light brightness-[0.9] sepia-[0.2]
|
|
transition-[top_transform] group-hover:-top-4 group-hover:scale-[.94]`,
|
|
cIf(thumbnailRounded, "rounded-md")
|
|
)}>
|
|
{thumbnail && (
|
|
<Img className="opacity-70" src={thumbnail} quality={ImageQuality.Medium} />
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{thumbnail ? (
|
|
<div
|
|
className="relative"
|
|
style={{
|
|
aspectRatio: thumbnailForceAspectRatio ? thumbnailAspectRatio : "unset",
|
|
}}>
|
|
<Img
|
|
className={cJoin(
|
|
cIf(
|
|
thumbnailRounded,
|
|
cIf(keepInfoVisible, "rounded-t-md", "rounded-md notHoverable:rounded-b-none")
|
|
),
|
|
cIf(thumbnailForceAspectRatio, "h-full w-full object-cover")
|
|
)}
|
|
src={thumbnail}
|
|
quality={ImageQuality.Medium}
|
|
/>
|
|
{stackNumber > 0 && (
|
|
<div
|
|
className="absolute right-2 top-2 rounded-full bg-black
|
|
bg-opacity-60 px-2 text-light">
|
|
{stackNumber}
|
|
</div>
|
|
)}
|
|
{hoverlay && hoverlay.__typename === "Video" && (
|
|
<>
|
|
<div
|
|
className="absolute inset-0 grid place-content-center bg-shade bg-opacity-0
|
|
text-light transition-colors drop-shadow-shade-lg group-hover:bg-opacity-50">
|
|
<Ico
|
|
icon={Icon.PlayCircleOutline}
|
|
className="!text-6xl opacity-0 transition-opacity group-hover:opacity-100"
|
|
/>
|
|
</div>
|
|
<div
|
|
className="absolute right-2 bottom-2 rounded-full bg-black bg-opacity-60 px-2
|
|
text-light">
|
|
{prettyDuration(hoverlay.duration)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div
|
|
style={{ aspectRatio: thumbnailAspectRatio }}
|
|
className={cJoin(
|
|
"relative w-full bg-light",
|
|
cIf(keepInfoVisible, "rounded-t-md", "rounded-md notHoverable:rounded-b-none")
|
|
)}>
|
|
{stackNumber > 0 && (
|
|
<div
|
|
className="absolute right-2 top-2 rounded-full bg-black
|
|
bg-opacity-60 px-2 text-light">
|
|
{stackNumber}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div
|
|
className={cJoin(
|
|
"z-20 grid gap-2 p-4 transition-opacity linearbg-obi",
|
|
cIf(
|
|
!keepInfoVisible && isHoverable,
|
|
`-inset-x-0.5 bottom-2 opacity-0 [border-radius:10%_10%_10%_10%_/_1%_1%_3%_3%]
|
|
group-hover:opacity-100 hoverable:absolute hoverable:drop-shadow-shade-lg
|
|
notHoverable:rounded-b-md notHoverable:opacity-100`,
|
|
"[border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]"
|
|
)
|
|
)}>
|
|
{metadata?.position === "Top" && metadataJSX}
|
|
{topChips && topChips.length > 0 && (
|
|
<div className="grid grid-flow-col place-content-start gap-1 overflow-hidden">
|
|
{topChips.map((text, index) => (
|
|
<Chip key={index} text={text} />
|
|
))}
|
|
</div>
|
|
)}
|
|
<div className="my-1">
|
|
{pre_title && <p className="mb-1 break-words leading-none">{pre_title}</p>}
|
|
{title && (
|
|
<p className="break-words font-headers text-lg font-bold leading-none">{title}</p>
|
|
)}
|
|
{subtitle && <p className="break-words leading-none">{subtitle}</p>}
|
|
</div>
|
|
{description && <p>{description}</p>}
|
|
{bottomChips && bottomChips.length > 0 && (
|
|
<div
|
|
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
|
[scrollbar-width:none] webkit-scrollbar:h-0">
|
|
{bottomChips.map((text, index) => (
|
|
<Chip key={index} className="text-sm" text={text} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{metadata?.position === "Bottom" && metadataJSX}
|
|
|
|
{infoAppend}
|
|
</div>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
/*
|
|
* ╭──────────────────────╮
|
|
* ───────────────────────────────────╯ TRANSLATED VARIANT ╰──────────────────────────────────────
|
|
*/
|
|
|
|
export const TranslatedPreviewCard = ({
|
|
translations,
|
|
fallback,
|
|
...otherProps
|
|
}: TranslatedProps<Props, "description" | "pre_title" | "subtitle" | "title">): JSX.Element => {
|
|
const [selectedTranslation] = useSmartLanguage({
|
|
items: translations,
|
|
languageExtractor: useCallback((item: { language: string }): string => item.language, []),
|
|
});
|
|
return (
|
|
<PreviewCard
|
|
pre_title={selectedTranslation?.pre_title ?? fallback.pre_title}
|
|
title={selectedTranslation?.title ?? fallback.title}
|
|
subtitle={selectedTranslation?.subtitle ?? fallback.subtitle}
|
|
description={selectedTranslation?.description ?? fallback.description}
|
|
{...otherProps}
|
|
/>
|
|
);
|
|
};
|