init 2 part media card

Part 1 is the default
Part 2 is more options pane
This commit is contained in:
Pas 2025-01-16 12:43:54 -07:00
parent 5160ebc189
commit 6cb781e452
3 changed files with 261 additions and 145 deletions

View file

@ -85,16 +85,6 @@ html[data-no-scroll], html[data-no-scroll] body {
min-height: 100dvh;
}
.info-button {
display: inline-block;
padding: 0.75em;
margin: -0.75em;
position: absolute;
bottom: 0;
right: 0;
transform: translate(-15px, -10px)
}
/*generated with Input range slider CSS style generator (version 20211225)
https://toughengineer.github.io/demo/slider-styler*/
:root {

View file

@ -72,6 +72,7 @@ export enum Icons {
STRETCH = "stretch",
CLOUD_ARROW_UP = "cloud_arrow_up",
FILE_ARROW_DOWN = "file_arrow_down",
ELLIPSIS = "ellipsis",
}
export interface IconProps {
@ -161,6 +162,7 @@ const iconList: Record<Icons, string> = {
stretch: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M344 0L488 0c13.3 0 24 10.7 24 24l0 144c0 9.7-5.8 18.5-14.8 22.2s-19.3 1.7-26.2-5.2l-39-39-87 87c-9.4 9.4-24.6 9.4-33.9 0l-32-32c-9.4-9.4-9.4-24.6 0-33.9l87-87L327 41c-6.9-6.9-8.9-17.2-5.2-26.2S334.3 0 344 0zM168 512L24 512c-13.3 0-24-10.7-24-24L0 344c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2l39 39 87-87c9.4-9.4 24.6-9.4 33.9 0l32 32c9.4 9.4 9.4 24.6 0 33.9l-87 87 39 39c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8z"/></svg>`,
cloud_arrow_up: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39V392c0 13.3 10.7 24 24 24s24-10.7 24-24V257.9l39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z" fill="currentColor"/></svg>`,
file_arrow_down: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM216 232V334.1l31-31c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-72 72c-9.4 9.4-24.6 9.4-33.9 0l-72-72c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l31 31V232c0-13.3 10.7-24 24-24s24 10.7 24 24z" fill="currentColor"/></svg>`,
ellipsis: `<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`,
};
function ChromeCastButton() {

View file

@ -1,5 +1,5 @@
import classNames from "classnames";
import { useCallback } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@ -48,7 +48,12 @@ function MediaCardContent({
percentage,
closable,
onClose,
}: MediaCardProps) {
overlayVisible,
setOverlayVisible,
}: MediaCardProps & {
overlayVisible: boolean;
setOverlayVisible: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { t } = useTranslation();
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
@ -68,142 +73,255 @@ function MediaCardContent({
dotListContent.push(t("media.unreleased"));
}
const handleMoreInfoClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const searchParam = encodeURIComponent(encodeURI(media.id));
const url =
media.type === "movie"
? `https://www.themoviedb.org/movie/${searchParam}`
: `https://www.themoviedb.org/tv/${searchParam}`;
window.open(url, "_blank");
};
const handleMouseLeave = () => {
setOverlayVisible(false);
};
return (
<Flare.Base
className={`group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10 ${
canLink ? "hover:bg-mediaCard-hoverBackground tabbable" : ""
}`}
tabIndex={canLink ? 0 : -1}
onKeyUp={(e) => e.key === "Enter" && e.currentTarget.click()}
>
<Flare.Light
flareSize={300}
cssColorVar="--colors-mediaCard-hoverAccent"
backgroundClass="bg-mediaCard-hoverBackground duration-100"
className={classNames({
"rounded-xl bg-background-main group-hover:opacity-100": canLink,
})}
/>
<Flare.Child
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
canLink ? "group-hover:scale-95" : "opacity-60"
}`}
>
<div
className={classNames(
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-300",
{
"group-hover:rounded-lg": canLink,
},
)}
style={{
backgroundImage: media.poster ? `url(${media.poster})` : undefined,
}}
<div>
{!overlayVisible ? (
<Flare.Base
className={`group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10 ${
canLink ? "hover:bg-mediaCard-hoverBackground tabbable" : ""
}`}
tabIndex={canLink ? 0 : -1}
onKeyUp={(e) => e.key === "Enter" && e.currentTarget.click()}
>
{series ? (
<div
className={[
"absolute right-2 top-2 rounded-md bg-mediaCard-badge px-2 py-1 transition-colors",
].join(" ")}
>
<p
className={[
"text-center text-xs font-bold text-mediaCard-badgeText transition-colors",
closable ? "" : "group-hover:text-white",
].join(" ")}
>
{t("media.episodeDisplay", {
season: series.season || 1,
episode: series.episode,
})}
</p>
</div>
) : null}
{percentage !== undefined ? (
<>
<div
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
}`}
/>
<div
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
}`}
/>
<div className="absolute inset-x-0 bottom-0 p-3">
<div className="relative h-1 overflow-hidden rounded-full bg-mediaCard-barColor">
<div
className="absolute inset-y-0 left-0 rounded-full bg-mediaCard-barFillColor"
style={{
width: percentageString,
}}
/>
</div>
</div>
</>
) : null}
<div
className="absolute bookmark-button"
onClick={(e) => e.preventDefault()}
>
<MediaBookmarkButton media={media} />
</div>
{searchQuery.length > 0 ? (
<div className="absolute" onClick={(e) => e.preventDefault()}>
<MediaBookmarkButton media={media} />
</div>
) : null}
<div
className={`absolute inset-0 flex items-center justify-center bg-mediaCard-badge bg-opacity-80 transition-opacity duration-500 ${
closable ? "opacity-100" : "pointer-events-none opacity-0"
<Flare.Light
flareSize={300}
cssColorVar="--colors-mediaCard-hoverAccent"
backgroundClass="bg-mediaCard-hoverBackground duration-100"
className={classNames({
"rounded-xl bg-background-main group-hover:opacity-100": canLink,
})}
/>
<Flare.Child
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
canLink ? "group-hover:scale-95" : "opacity-60"
}`}
>
<IconPatch
clickable
className="text-2xl text-mediaCard-badgeText transition-transform hover:scale-110 duration-500"
onClick={() => closable && onClose?.()}
icon={Icons.X}
/>
</div>
</div>
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
<span>{media.title}</span>
</h1>
<div className="media-info-container justify-content-center flex flex-wrap">
<DotList className="text-xs" content={dotListContent} />
<button
className="info-button"
type="button"
onClick={(e) => {
e.preventDefault();
<div
className={classNames(
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-300",
{
"group-hover:rounded-lg": canLink,
},
)}
style={{
backgroundImage: media.poster
? `url(${media.poster})`
: undefined,
}}
>
{series ? (
<div
className={[
"absolute right-2 top-2 rounded-md bg-mediaCard-badge px-2 py-1 transition-colors",
].join(" ")}
>
<p
className={[
"text-center text-xs font-bold text-mediaCard-badgeText transition-colors",
closable ? "" : "group-hover:text-white",
].join(" ")}
>
{t("media.episodeDisplay", {
season: series.season || 1,
episode: series.episode,
})}
</p>
</div>
) : null}
const searchParam = encodeURIComponent(encodeURI(media.id));
const url =
media.type === "movie"
? `https://www.themoviedb.org/movie/${searchParam}`
: `https://www.themoviedb.org/tv/${searchParam}`;
{percentage !== undefined ? (
<>
<div
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
}`}
/>
<div
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
}`}
/>
<div className="absolute inset-x-0 bottom-0 p-3">
<div className="relative h-1 overflow-hidden rounded-full bg-mediaCard-barColor">
<div
className="absolute inset-y-0 left-0 rounded-full bg-mediaCard-barFillColor"
style={{
width: percentageString,
}}
/>
</div>
</div>
</>
) : null}
window.open(url, "_blank");
}}
>
<Icon
className="text-xs font-semibold text-type-secondary"
icon={Icons.CIRCLE_QUESTION}
<div
className="absolute bookmark-button"
onClick={(e) => e.preventDefault()}
>
<MediaBookmarkButton media={media} />
</div>
{searchQuery.length > 0 ? (
<div className="absolute" onClick={(e) => e.preventDefault()}>
<MediaBookmarkButton media={media} />
</div>
) : null}
<div
className={`absolute inset-0 flex items-center justify-center bg-mediaCard-badge bg-opacity-80 transition-opacity duration-500 ${
closable ? "opacity-100" : "pointer-events-none opacity-0"
}`}
>
<IconPatch
clickable
className="text-2xl text-mediaCard-badgeText transition-transform hover:scale-110 duration-500"
onClick={() => closable && onClose?.()}
icon={Icons.X}
/>
</div>
</div>
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
<span>{media.title}</span>
</h1>
<div className="media-info-container justify-content-center flex flex-wrap">
<DotList className="text-xs" content={dotListContent} />
</div>
{/* More Info */}
<div className="absolute bottom-1 right-2">
<button
className="media-more-button"
type="button"
onClick={(e) => {
e.preventDefault();
setOverlayVisible(!overlayVisible);
}}
>
<Icon
className="text-xs font-semibold text-type-secondary"
icon={Icons.ELLIPSIS}
/>
</button>
</div>
{/* End Overlay */}
</Flare.Child>
</Flare.Base>
) : (
<div onMouseLeave={handleMouseLeave}>
<Flare.Base className="group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10">
<Flare.Light
flareSize={300}
cssColorVar="--colors-mediaCard-hoverAccent"
backgroundClass="bg-mediaCard-hoverBackground duration-100"
className={classNames({
"rounded-xl bg-background-main group-hover:opacity-100":
canLink,
})}
/>
</button>
<Flare.Child
className={`pointer-events-auto relative mb-2 p-[0.4em] transition-transform duration-300 ${
canLink ? "group-hover:scale-95" : "opacity-60"
}`}
>
<div
className={classNames(
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-300",
{
"group-hover:rounded-lg": canLink,
},
"blur-sm",
)}
style={{
backgroundImage: media.poster
? `url(${media.poster})`
: undefined,
}}
>
{series ? (
<div
className={[
"absolute right-2 top-2 rounded-md bg-mediaCard-badge px-2 py-1 transition-colors",
].join(" ")}
>
<p
className={[
"text-center text-xs font-bold text-mediaCard-badgeText transition-colors",
closable ? "" : "group-hover:text-white",
].join(" ")}
>
{t("media.episodeDisplay", {
season: series.season || 1,
episode: series.episode,
})}
</p>
</div>
) : null}
{percentage !== undefined ? (
<>
<div
className={`absolute inset-x-0 -bottom-px pb-1 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
}`}
/>
<div
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-mediaCard-shadow to-transparent transition-colors ${
canLink ? "group-hover:from-mediaCard-hoverShadow" : ""
}`}
/>
<div className="absolute inset-x-0 bottom-0 p-3">
<div className="relative h-1 overflow-hidden rounded-full bg-mediaCard-barColor">
<div
className="absolute inset-y-0 left-0 rounded-full bg-mediaCard-barFillColor"
style={{
width: percentageString,
}}
/>
</div>
</div>
</>
) : null}
</div>
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
<span>{media.title}</span>
</h1>
<div className="media-info-container justify-content-center flex flex-wrap">
<DotList className="text-xs" content={dotListContent} />
</div>
</Flare.Child>
</Flare.Base>
</div>
</Flare.Child>
</Flare.Base>
)}
</div>
);
}
export function MediaCard(props: MediaCardProps) {
const content = <MediaCardContent {...props} />;
const [overlayVisible, setOverlayVisible] = useState(false);
const content = (
<MediaCardContent
{...props}
overlayVisible={overlayVisible}
setOverlayVisible={setOverlayVisible}
/>
);
const isReleased = useCallback(
() => checkReleased(props.media),
@ -227,15 +345,21 @@ export function MediaCard(props: MediaCardProps) {
if (!canLink) return <span>{content}</span>;
return (
<Link
to={link}
tabIndex={-1}
className={classNames(
"tabbable",
props.closable ? "hover:cursor-default" : "",
<div className="relative">
{!overlayVisible ? (
<Link
to={link}
tabIndex={-1}
className={classNames(
"tabbable",
props.closable ? "hover:cursor-default" : "",
)}
>
{content}
</Link>
) : (
<div>{content}</div>
)}
>
{content}
</Link>
</div>
);
}