Image Metadata Preview
Image Metadata Preview é um componente de media para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
media
O que é
Image Metadata Preview é um componente de media para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion lucide-react react-use-measure
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Image Metadata Preview
"use client";
import { ChevronUp, CircleX, Share } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useState } from "react";
import useMeasure from "react-use-measure";
export interface ImageMetadata {
by: string;
created: string;
source: string;
updated: string;
}
export interface ImageMetadataPreviewProps {
alt?: string;
description?: string;
filename?: string;
imageSrc: string;
metadata: ImageMetadata;
onShare?: () => void;
}
export default function ImageMetadataPreview({
imageSrc,
alt = "Image preview",
filename = "screenshot.png",
description = "No description",
metadata,
onShare,
}: ImageMetadataPreviewProps) {
const [openInfo, setopenInfo] = useState(false);
const [isHoverDevice, setIsHoverDevice] = useState(false);
const [elementRef, bounds] = useMeasure();
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
const mediaQuery = window.matchMedia("(hover: hover) and (pointer: fine)");
setIsHoverDevice(mediaQuery.matches);
const handleChange = (e: MediaQueryListEvent) => {
setIsHoverDevice(e.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);
const handleClickOpen = () => {
setopenInfo((b) => !b);
};
const handleClickClose = () => {
setopenInfo((b) => !b);
};
return (
<div className="absolute bottom-10 flex flex-col items-center justify-center gap-4">
<motion.div
animate={shouldReduceMotion ? {} : { y: -bounds.height }}
className="pointer-events-none overflow-hidden rounded-xl"
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }}
>
<img
alt={alt}
draggable={false}
height={437}
src={imageSrc}
width={300}
/>
</motion.div>
<div className="relative flex w-full flex-col items-center gap-4">
<div className="relative flex w-full flex-row items-center justify-center gap-4">
<button
aria-label="Share"
className={`min-h-[44px] min-w-[44px] rounded-full border bg-background p-3 transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
isHoverDevice ? "hover:bg-muted" : ""
}`}
disabled={!onShare}
onClick={onShare}
type="button"
>
<Share aria-hidden="true" size={16} />
</button>
<button
aria-label="Connect"
className="min-h-[44px] cursor-not-allowed rounded-full border bg-background px-4 py-3 text-sm transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50"
disabled
type="button"
>
Connect
</button>
<AnimatePresence>
{openInfo ? null : (
<motion.button
animate={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(0px)", opacity: 1 }
}
aria-label="Open Metadata Preview"
className={`min-h-[44px] min-w-[44px] border bg-background p-3 shadow-xs transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
isHoverDevice ? "hover:bg-muted" : ""
}`}
initial={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(4px)", opacity: 0 }
}
onClick={handleClickOpen}
style={{ borderRadius: 100 }}
transition={
shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }
}
>
<ChevronUp aria-hidden="true" size={16} />
</motion.button>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{openInfo ? (
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(0px)", opacity: 1 }
}
className="absolute bottom-0 w-full cursor-pointer gap-4 border bg-background p-5 shadow-xs"
initial={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(4px)", opacity: 0 }
}
onClick={handleClickClose}
style={{ borderRadius: 20 }}
transition={
shouldReduceMotion
? { duration: 0 }
: { bounce: 0, duration: 0.25, type: "spring" as const }
}
>
<div className="flex flex-col items-start" ref={elementRef}>
<div className="flex w-full flex-row items-start justify-between gap-4">
<div>
<p className="text-foreground">{filename}</p>
<p className="text-primary-foreground">{description}</p>
</div>
<button
aria-label="Close metadata preview"
className={`flex min-h-[44px] min-w-[44px] items-center justify-center rounded p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
isHoverDevice ? "hover:bg-muted" : ""
}`}
onClick={(e) => {
e.stopPropagation();
handleClickClose();
}}
type="button"
>
<CircleX aria-hidden="true" size={16} />
</button>
</div>
<table className="flex w-full flex-col items-center gap-4 text-foreground">
<tbody className="w-full">
<tr className="flex w-full flex-row items-center gap-4">
<td className="w-1/2">Created</td>
<td className="w-1/2 text-primary-foreground">
{metadata.created}
</td>
</tr>
<tr className="flex w-full flex-row items-center gap-4">
<td className="w-1/2">Updated</td>
<td className="w-1/2 text-primary-foreground">
{metadata.updated}
</td>
</tr>
<tr className="flex w-full flex-row items-center gap-4">
<td className="w-1/2">By</td>
<td className="w-1/2">{metadata.by}</td>
</tr>
<tr className="flex w-full flex-row items-center gap-4">
<td className="w-1/2">Source</td>
<td className="w-1/2 truncate">{metadata.source}</td>
</tr>
</tbody>
</table>
</div>
</motion.div>
) : null}
</AnimatePresence>
</div>
</div>
);
}