Files
pinkudex/components/common/HoverImagePreview.tsx
2026-05-26 22:46:00 +02:00

83 lines
2.2 KiB
TypeScript

"use client";
import { useRef, useState, useEffect } from "react";
import { imageUrl } from "@/lib/assetUrls";
/**
* Wraps any element. On hover of the wrapped child, a floating popover
* shows the full-resolution image at /api/image/[imageId].
*/
export function HoverImagePreview({
imageId,
children,
}: {
imageId: number;
children: React.ReactNode;
}) {
const [show, setShow] = useState(false);
const [pos, setPos] = useState<{ maxW: number; maxH: number }>({ maxW: 0, maxH: 0 });
const ref = useRef<HTMLSpanElement>(null);
const timerRef = useRef<number | null>(null);
const open = () => {
const margin = 24;
setPos({
maxW: window.innerWidth - margin * 2,
maxH: window.innerHeight - margin * 2,
});
timerRef.current = window.setTimeout(() => setShow(true), 120);
};
const close = () => {
if (timerRef.current != null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
setShow(false);
};
useEffect(() => () => { if (timerRef.current != null) clearTimeout(timerRef.current); }, []);
return (
<>
<span
ref={ref}
onMouseEnter={open}
onMouseLeave={close}
onFocus={open}
onBlur={close}
className="inline-block"
>
{children}
</span>
{show && (
<div
className="fixed z-[100] pointer-events-none rounded-xl overflow-hidden border border-[var(--color-glass-border-strong)] shadow-2xl"
style={{
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: pos.maxW,
maxHeight: pos.maxH,
background: "color-mix(in oklch, var(--color-bg-0) 92%, transparent)",
backdropFilter: "blur(20px)",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageUrl({ id: imageId, code: null })}
alt=""
style={{
maxWidth: pos.maxW,
maxHeight: pos.maxH,
width: "auto",
height: "auto",
display: "block",
objectFit: "contain",
}}
/>
</div>
)}
</>
);
}