Files
v2.app.gaertan.art/src/components/artworks/ArtworkImageCard.tsx

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>
);
}