2023-02-16 09:19:44 +01:00

251 lines
9.4 KiB
TypeScript

import { MouseEventHandler, useCallback } from "react";
import { useRouter } from "next/router";
import { Markdown } from "./Markdown/Markdown";
import { Chip } from "components/Chip";
import { Ico } from "components/Ico";
import { Img } from "components/Img";
import { UpPressable } from "components/Containers/UpPressable";
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 { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
* ╭─────────────╮
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
*/
interface Props {
thumbnail?: UploadImageFragment | string | null | undefined;
thumbnailAspectRatio?: string;
thumbnailForceAspectRatio?: boolean;
thumbnailFitMethod?: "contain" | "cover";
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;
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" };
disabled?: boolean;
className?: string;
onClick?: MouseEventHandler<HTMLAnchorElement>;
}
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const PreviewCard = ({
href,
thumbnail,
thumbnailAspectRatio = "4/3",
thumbnailForceAspectRatio = false,
thumbnailFitMethod = "cover",
thumbnailRounded = true,
pre_title,
title,
subtitle,
description,
topChips,
bottomChips,
keepInfoVisible,
metadata,
hoverlay,
infoAppend,
className,
disabled = false,
onClick,
}: Props): JSX.Element => {
const currency = useAtomGetter(atoms.settings.currency);
const currencies = useAtomGetter(atoms.localData.currencies);
const isHoverable = useDeviceSupportsHover();
const router = useRouter();
const metadataJSX = (
<>
{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="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="shopping_cart" className="mr-1 translate-y-[.15em] !text-base" />
{prettyPrice(metadata.price, currencies, currency)}
</p>
)}
{metadata.views && (
<p className="text-sm">
<Ico icon="visibility" className="mr-1 translate-y-[.15em] !text-base" />
{prettyShortenNumber(metadata.views)}
</p>
)}
{metadata.author && (
<p className="text-sm">
<Ico icon="person" className="mr-1 translate-y-[.15em] !text-base" />
<Markdown text={metadata.author} className="inline-block" />
</p>
)}
</div>
)}
</>
);
return (
<UpPressable
className={cJoin("grid items-end text-left", className)}
href={href}
onClick={onClick}
noBackground
disabled={disabled}>
<div className={cJoin("group", cIf(disabled, "pointer-events-none touch-none select-none"))}>
{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"),
cIf(
thumbnailForceAspectRatio && thumbnailFitMethod === "contain",
"object-contain",
"object-cover"
)
)}
src={thumbnail}
quality={ImageQuality.Medium}
/>
{hoverlay && hoverlay.__typename === "Video" && (
<>
<div
className="absolute inset-0 grid place-content-center rounded-t-md
bg-shade/0 text-light transition-colors group-hover:bg-shade/50">
<Ico
icon="play_circle"
className="!text-6xl text-light opacity-0 drop-shadow-lg transition-opacity
shadow-shade group-hover:opacity-100 dark:text-black"
/>
</div>
<div className="absolute right-2 bottom-2 rounded-full bg-black/60 px-2 text-light">
{prettyDuration(hoverlay.duration)}
</div>
</>
)}
</div>
) : (
<div
style={{ aspectRatio: thumbnailAspectRatio }}
className={cJoin(
"relative w-full bg-highlight",
cIf(keepInfoVisible, "rounded-t-md", "rounded-md notHoverable:rounded-b-none")
)}
/>
)}
<div
className={cJoin(
"z-20 gap-2 p-4 transition-opacity linearbg-obi",
cIf(
!keepInfoVisible && isHoverable,
`-inset-x-0.5 bottom-2 opacity-0 shadow-shade
[border-radius:10%_10%_10%_10%_/_1%_1%_3%_3%]
group-hover:opacity-100 hoverable:absolute hoverable:drop-shadow-lg
notHoverable:rounded-b-md notHoverable:opacity-100`,
"grid [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-x-scroll
scrollbar-none">
{topChips.map((text, index) => (
<Chip key={index} text={text} />
))}
</div>
)}
<div className="my-1">
{pre_title && <Markdown text={pre_title} className="mb-1 leading-none break-words" />}
{title && (
<Markdown
text={title}
className="font-headers text-lg font-bold leading-none break-words"
/>
)}
{subtitle && <Markdown text={subtitle} className="leading-none break-words" />}
</div>
{description && <Markdown text={description} className="break-words" />}
{bottomChips && bottomChips.length > 0 && (
<div
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
scrollbar-none">
{bottomChips.map((text, index) => (
<Chip key={index} className="text-sm" text={text} />
))}
</div>
)}
{metadata?.position === "Bottom" && metadataJSX}
{infoAppend}
</div>
</div>
</UpPressable>
);
};
/*
* ╭──────────────────────╮
* ───────────────────────────────────╯ 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}
/>
);
};