← back to Nineoh Guide
apps/web/components/RightsImage.tsx
78 lines
import { type Asset, assetIsRenderable } from "@nineoh/core";
/**
* The pending-image placeholder pipeline.
*
* An image renders ONLY if it has a cleared, non-PENDING license and a file.
* Otherwise we show a polished gradient placeholder with a PENDING pill — the
* page never looks broken while an image license is still drafted for approval.
* This is the UI half of the rights-ledger enforced in the assets table.
*/
export function RightsImage({
asset,
alt,
width = 320,
height = 200,
}: {
asset: Asset | null | undefined;
alt: string;
width?: number;
height?: number;
}) {
if (asset && assetIsRenderable(asset)) {
return (
<figure style={{ margin: 0 }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={asset.fileUrl!}
alt={alt}
width={width}
height={height}
style={{ objectFit: "cover", borderRadius: 8, display: "block" }}
/>
{asset.attributionText ? (
<figcaption style={{ fontSize: 11, color: "#888", marginTop: 4 }}>
{asset.attributionText}
</figcaption>
) : null}
</figure>
);
}
// Deterministic hue from the alt text so placeholders look intentional.
const hue =
Math.abs(
Array.from(alt).reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7)
) % 360;
return (
<div
role="img"
aria-label={`${alt} (image pending license clearance)`}
style={{
width,
height,
borderRadius: 8,
background: `linear-gradient(135deg, hsl(${hue} 45% 82%), hsl(${(hue + 40) % 360} 45% 68%))`,
display: "flex",
alignItems: "flex-end",
padding: 8,
boxSizing: "border-box",
}}
>
<span
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: 0.5,
color: "#fff",
background: "rgba(0,0,0,0.35)",
padding: "2px 6px",
borderRadius: 4,
}}
>
IMAGE PENDING
</span>
</div>
);
}