134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
import Image from "next/image";
|
|
import Link from "next/link";
|
|
|
|
type ClampOpts = { maxIntrinsic: number };
|
|
|
|
/**
|
|
* Clamps intrinsic w/h while preserving aspect ratio.
|
|
* This helps prevent Next/Image from treating a tiny thumbnail as a large asset.
|
|
*/
|
|
function clampDims(w: number, h: number, { maxIntrinsic }: ClampOpts) {
|
|
const W = Math.max(1, w);
|
|
const H = Math.max(1, h);
|
|
const maxSide = Math.max(W, H);
|
|
if (maxSide <= maxIntrinsic) return { w: W, h: H };
|
|
|
|
const scale = maxIntrinsic / maxSide;
|
|
return {
|
|
w: Math.max(1, Math.round(W * scale)),
|
|
h: Math.max(1, Math.round(H * scale)),
|
|
};
|
|
}
|
|
|
|
export type ArtworkImageCardProps = {
|
|
href: string;
|
|
src: string;
|
|
alt: string;
|
|
|
|
/**
|
|
* Provide real dimensions if you have them.
|
|
* For tile mode, we still pass intrinsic sizes, but clamp them.
|
|
*/
|
|
width?: number;
|
|
height?: number;
|
|
|
|
/**
|
|
* tile: uses `fill` + container aspect ratio (best for masonry/grid)
|
|
* hero: uses width/height when available (best for single page)
|
|
*/
|
|
mode?: "tile" | "hero";
|
|
|
|
/**
|
|
* For tile mode only.
|
|
* Example: "4 / 3" or `${w} / ${h}`
|
|
*/
|
|
aspectRatio?: string;
|
|
|
|
className?: string;
|
|
imageClassName?: string;
|
|
|
|
/**
|
|
* Controls intrinsic clamp for thumbnails:
|
|
* you asked: “max the double of 160” => 320 default.
|
|
*/
|
|
maxThumbIntrinsic?: number;
|
|
|
|
sizes?: string;
|
|
priority?: boolean;
|
|
style?: React.CSSProperties;
|
|
linkClassName?: string;
|
|
};
|
|
|
|
export function ArtworkImageCard({
|
|
href,
|
|
src,
|
|
alt,
|
|
width,
|
|
height,
|
|
mode = "tile",
|
|
aspectRatio = "4 / 3",
|
|
className,
|
|
imageClassName,
|
|
maxThumbIntrinsic = 320,
|
|
sizes,
|
|
priority,
|
|
style,
|
|
}: ArtworkImageCardProps) {
|
|
const w = width ?? 0;
|
|
const h = height ?? 0;
|
|
|
|
const isHero = mode === "hero";
|
|
const hasDims = w > 0 && h > 0;
|
|
|
|
// Clamp only for non-hero (thumbnail-like) usage
|
|
const clamped = !isHero && hasDims ? clampDims(w, h, { maxIntrinsic: maxThumbIntrinsic }) : null;
|
|
|
|
return (
|
|
<div
|
|
style={style}
|
|
className={[
|
|
"group rounded-lg overflow-hidden bg-background relative",
|
|
// IMPORTANT: put the border here so the --dom hover works again
|
|
"border border-transparent transition-colors duration-150 hover:border-(--dom)",
|
|
"hover:shadow-lg transition-shadow",
|
|
className ?? "",
|
|
].join(" ")}
|
|
>
|
|
<div
|
|
className="relative w-full bg-muted items-center justify-center"
|
|
style={mode === "tile" ? { aspectRatio } : undefined}
|
|
>
|
|
<Link href={href} className="block">
|
|
{isHero ? (
|
|
<Image
|
|
src={src}
|
|
alt={alt}
|
|
// If we have dimensions, use them; otherwise fallback to fill.
|
|
width={hasDims ? w : undefined}
|
|
height={hasDims ? h : undefined}
|
|
fill={!hasDims}
|
|
className={["object-cover transition duration-300", imageClassName ?? ""].join(" ")}
|
|
sizes={sizes}
|
|
priority={priority}
|
|
quality={100}
|
|
/>
|
|
) : (
|
|
<Image
|
|
src={src}
|
|
alt={alt}
|
|
// Pass clamped intrinsic sizes when possible
|
|
width={clamped?.w ?? 320}
|
|
height={clamped?.h ?? 240}
|
|
className={["w-full h-full object-cover select-none", imageClassName ?? ""].join(" ")}
|
|
loading={priority ? "eager" : "lazy"}
|
|
sizes={sizes}
|
|
priority={priority}
|
|
quality={100}
|
|
/>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|