Refactor galleries and single page
This commit is contained in:
131
src/components/artworks/ArtworkImageCard.tsx
Normal file
131
src/components/artworks/ArtworkImageCard.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user