Skip to content

Commit b50e3cc

Browse files
committed
Adding link preview component
1 parent b4027cd commit b50e3cc

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

components/ui/link-preview.tsx

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"use client";
2+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3+
import Image from "next/image";
4+
import { encode } from "qss";
5+
import React from "react";
6+
import {
7+
AnimatePresence,
8+
motion,
9+
useMotionValue,
10+
useSpring,
11+
} from "framer-motion";
12+
import Link from "next/link";
13+
import { cn } from "@/utils/cn";
14+
15+
type LinkPreviewProps = {
16+
children: React.ReactNode;
17+
url: string;
18+
className?: string;
19+
width?: number;
20+
height?: number;
21+
quality?: number;
22+
layout?: string;
23+
} & (
24+
| { isStatic: true; imageSrc: string }
25+
| { isStatic?: false; imageSrc?: never }
26+
);
27+
28+
export const LinkPreview = ({
29+
children,
30+
url,
31+
className,
32+
width = 200,
33+
height = 125,
34+
quality = 50,
35+
layout = "fixed",
36+
isStatic = false,
37+
imageSrc = "",
38+
}: LinkPreviewProps) => {
39+
let src;
40+
if (!isStatic) {
41+
const params = encode({
42+
url,
43+
screenshot: true,
44+
meta: false,
45+
embed: "screenshot.url",
46+
colorScheme: "dark",
47+
"viewport.isMobile": true,
48+
"viewport.deviceScaleFactor": 1,
49+
"viewport.width": width * 3,
50+
"viewport.height": height * 3,
51+
});
52+
src = `https://api.microlink.io/?${params}`;
53+
} else {
54+
src = imageSrc;
55+
}
56+
57+
const [isOpen, setOpen] = React.useState(false);
58+
59+
const [isMounted, setIsMounted] = React.useState(false);
60+
61+
React.useEffect(() => {
62+
setIsMounted(true);
63+
}, []);
64+
65+
const springConfig = { stiffness: 100, damping: 15 };
66+
const x = useMotionValue(0);
67+
68+
const translateX = useSpring(x, springConfig);
69+
70+
const handleMouseMove = (event: any) => {
71+
const targetRect = event.target.getBoundingClientRect();
72+
const eventOffsetX = event.clientX - targetRect.left;
73+
const offsetFromCenter = (eventOffsetX - targetRect.width / 2) / 2; // Reduce the effect to make it subtle
74+
x.set(offsetFromCenter);
75+
};
76+
77+
return (
78+
<>
79+
{isMounted ? (
80+
<div className="hidden">
81+
<Image
82+
src={src}
83+
width={width}
84+
height={height}
85+
quality={quality}
86+
layout={layout}
87+
priority={true}
88+
alt="hidden image"
89+
/>
90+
</div>
91+
) : null}
92+
93+
<HoverCardPrimitive.Root
94+
openDelay={50}
95+
closeDelay={100}
96+
onOpenChange={(open) => {
97+
setOpen(open);
98+
}}
99+
>
100+
<HoverCardPrimitive.Trigger
101+
onMouseMove={handleMouseMove}
102+
className={cn("text-black dark:text-white", className)}
103+
href={url}
104+
>
105+
{children}
106+
</HoverCardPrimitive.Trigger>
107+
108+
<HoverCardPrimitive.Content
109+
className="[transform-origin:var(--radix-hover-card-content-transform-origin)]"
110+
side="top"
111+
align="center"
112+
sideOffset={10}
113+
>
114+
<AnimatePresence>
115+
{isOpen && (
116+
<motion.div
117+
initial={{ opacity: 0, y: 20, scale: 0.6 }}
118+
animate={{
119+
opacity: 1,
120+
y: 0,
121+
scale: 1,
122+
transition: {
123+
type: "spring",
124+
stiffness: 260,
125+
damping: 20,
126+
},
127+
}}
128+
exit={{ opacity: 0, y: 20, scale: 0.6 }}
129+
className="shadow-xl rounded-xl"
130+
style={{
131+
x: translateX,
132+
}}
133+
>
134+
<Link
135+
href={url}
136+
className="block p-1 bg-white border-2 border-transparent shadow rounded-xl hover:border-neutral-200 dark:hover:border-neutral-800"
137+
style={{ fontSize: 0 }}
138+
>
139+
<Image
140+
src={isStatic ? imageSrc : src}
141+
width={width}
142+
height={height}
143+
quality={quality}
144+
layout={layout}
145+
priority={true}
146+
className="rounded-lg"
147+
alt="preview image"
148+
/>
149+
</Link>
150+
</motion.div>
151+
)}
152+
</AnimatePresence>
153+
</HoverCardPrimitive.Content>
154+
</HoverCardPrimitive.Root>
155+
</>
156+
);
157+
};

0 commit comments

Comments
 (0)