Skip to content

Commit 8b906e9

Browse files
committed
Add Link Preview and Rough Notation
1 parent a0166b7 commit 8b906e9

File tree

11 files changed

+709
-18
lines changed

11 files changed

+709
-18
lines changed

components/LinkPreview.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { FOCUS_VISIBLE_OUTLINE, GRADIENT_LINK } from "@/lib/constants";
2+
import { Portal, Transition } from "@headlessui/react";
3+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
4+
import cx from "clsx";
5+
import Image from "next/image";
6+
import { encode } from "qss";
7+
import React from "react";
8+
9+
export const LinkPreview = ({ children, url }) => {
10+
const width = 200;
11+
const height = 125;
12+
const quality = 50;
13+
const layout = "fixed";
14+
15+
// Simplifies things by encoding our microlink params into a query string.
16+
const params = encode({
17+
url,
18+
screenshot: true,
19+
meta: false,
20+
embed: "screenshot.url",
21+
colorScheme: "dark",
22+
"viewport.isMobile": true,
23+
"viewport.deviceScaleFactor": 1,
24+
25+
// To capture useful content, the screenshot viewport needs to be bigger
26+
// than our images but maintain the same ratio
27+
"viewport.width": width * 3,
28+
"viewport.height": height * 3,
29+
});
30+
31+
const src = `https://api.microlink.io/?${params}`;
32+
33+
const [isOpen, setOpen] = React.useState(false);
34+
// const [static, setStatic] = useState(false);
35+
36+
// if (staticImage) setStatic(true);
37+
38+
const [isMounted, setIsMounted] = React.useState(false);
39+
40+
React.useEffect(() => {
41+
setIsMounted(true);
42+
}, []);
43+
44+
return (
45+
<>
46+
{/**
47+
* Microlink.io + next/image can take a few seconds to fetch and generate
48+
* a screenshot. The delay makes <LinkPreview> pointless. As a hacky
49+
* solution we create a second <Image> in a Portal after the component has
50+
* mounted. This <Image> triggers microlink.io + next/image so that the
51+
* image itself is ready by the time the user hovers on a <LinkPreview>.
52+
* Not concerned about the performance impact because <Image>'s are cached
53+
* after they are generated and the images themselves are tiny (< 10kb).
54+
*/}
55+
{isMounted ? (
56+
<Portal>
57+
<div className="hidden">
58+
<Image
59+
src={src}
60+
width={width}
61+
height={height}
62+
quality={quality}
63+
layout={layout}
64+
priority={true}
65+
/>
66+
</div>
67+
</Portal>
68+
) : null}
69+
70+
<HoverCardPrimitive.Root
71+
openDelay={50}
72+
onOpenChange={(open) => {
73+
setOpen(open);
74+
}}
75+
>
76+
<HoverCardPrimitive.Trigger
77+
href={url}
78+
className={cx(GRADIENT_LINK, FOCUS_VISIBLE_OUTLINE)}
79+
>
80+
{children}
81+
</HoverCardPrimitive.Trigger>
82+
83+
<HoverCardPrimitive.Content side="top" align="center" sideOffset={10}>
84+
<Transition
85+
show={isOpen}
86+
appear={true}
87+
enter="transform transition duration-300 origin-bottom ease-out"
88+
enterFrom="opacity-0 translate-y-2 scale-0"
89+
enterTo="opacity-100 translate-y-0 scale-100"
90+
className="shadow-xl rounded-xl"
91+
>
92+
<a
93+
href={url}
94+
className="block p-1 bg-white border border-transparent shadow rounded-xl hover:border-pink-500"
95+
// Unfortunate hack to remove the weird whitespace left by
96+
// next/image wrapper div
97+
// https://github.com/vercel/next.js/issues/18915
98+
style={{ fontSize: 0 }}
99+
>
100+
<Image
101+
src={src}
102+
width={width}
103+
height={height}
104+
quality={quality}
105+
layout={layout}
106+
priority={true}
107+
className="rounded-lg"
108+
/>
109+
</a>
110+
</Transition>
111+
</HoverCardPrimitive.Content>
112+
</HoverCardPrimitive.Root>
113+
</>
114+
);
115+
};

components/StaticLinkPreview.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { FOCUS_VISIBLE_OUTLINE, GRADIENT_LINK } from "@/lib/constants";
2+
import { Portal, Transition } from "@headlessui/react";
3+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
4+
import cx from "clsx";
5+
import Image from "next/image";
6+
import { encode } from "qss";
7+
import React from "react";
8+
9+
export const StaticLinkPreview = ({ children, url }) => {
10+
const width = 200;
11+
const height = 125;
12+
const quality = 50;
13+
const layout = "fixed";
14+
15+
// Simplifies things by encoding our microlink params into a query string.
16+
const params = encode({
17+
url,
18+
screenshot: true,
19+
meta: false,
20+
embed: "screenshot.url",
21+
colorScheme: "dark",
22+
"viewport.isMobile": true,
23+
"viewport.deviceScaleFactor": 1,
24+
25+
// To capture useful content, the screenshot viewport needs to be bigger
26+
// than our images but maintain the same ratio
27+
"viewport.width": width * 3,
28+
"viewport.height": height * 3,
29+
});
30+
31+
const src = `https://api.microlink.io/?${params}`;
32+
33+
const [isOpen, setOpen] = React.useState(false);
34+
// const [static, setStatic] = useState(false);
35+
36+
// if (staticImage) setStatic(true);
37+
38+
const [isMounted, setIsMounted] = React.useState(false);
39+
40+
React.useEffect(() => {
41+
setIsMounted(true);
42+
}, []);
43+
44+
return (
45+
<>
46+
{/**
47+
* Microlink.io + next/image can take a few seconds to fetch and generate
48+
* a screenshot. The delay makes <LinkPreview> pointless. As a hacky
49+
* solution we create a second <Image> in a Portal after the component has
50+
* mounted. This <Image> triggers microlink.io + next/image so that the
51+
* image itself is ready by the time the user hovers on a <LinkPreview>.
52+
* Not concerned about the performance impact because <Image>'s are cached
53+
* after they are generated and the images themselves are tiny (< 10kb).
54+
*/}
55+
{isMounted ? (
56+
<Portal>
57+
<div className="hidden">
58+
<Image
59+
src={src}
60+
width={width}
61+
height={height}
62+
quality={quality}
63+
layout={layout}
64+
priority={true}
65+
/>
66+
</div>
67+
</Portal>
68+
) : null}
69+
70+
<HoverCardPrimitive.Root
71+
openDelay={50}
72+
onOpenChange={(open) => {
73+
setOpen(open);
74+
}}
75+
>
76+
<HoverCardPrimitive.Trigger
77+
href={url}
78+
className={cx(GRADIENT_LINK, FOCUS_VISIBLE_OUTLINE)}
79+
>
80+
{children}
81+
</HoverCardPrimitive.Trigger>
82+
83+
<HoverCardPrimitive.Content side="top" align="center" sideOffset={10}>
84+
<Transition
85+
show={isOpen}
86+
appear={true}
87+
enter="transform transition duration-300 origin-bottom ease-out"
88+
enterFrom="opacity-0 translate-y-2 scale-0"
89+
enterTo="opacity-100 translate-y-0 scale-100"
90+
className="shadow-xl rounded-xl"
91+
>
92+
<span
93+
className="block p-1 bg-white border border-transparent shadow rounded-xl hover:border-pink-500"
94+
// Unfortunate hack to remove the weird whitespace left by
95+
// next/image wrapper div
96+
// https://github.com/vercel/next.js/issues/18915
97+
style={{ fontSize: 0 }}
98+
>
99+
<Image
100+
src={src}
101+
width={width}
102+
height={height}
103+
quality={quality}
104+
layout={layout}
105+
priority={true}
106+
className="rounded-lg"
107+
/>
108+
</span>
109+
</Transition>
110+
</HoverCardPrimitive.Content>
111+
</HoverCardPrimitive.Root>
112+
</>
113+
);
114+
};

jsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"@/data/*": ["data/*"],
77
"@/layouts/*": ["layouts/*"],
88
"@/lib/*": ["lib/*"],
9-
"@/styles/*": ["styles/*"]
9+
"@/styles/*": ["styles/*"],
10+
"@/ui/*": ["ui/*"],
1011
}
1112
}
1213
}

lib/constants.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const FOCUS_VISIBLE_OUTLINE = `focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-lightBlue-500 focus-visible:ring-opacity-50 focus-visible:outline-none focus:outline-none`;
2+
3+
export const GRADIENT_LINK = `decoration-clone bg-clip-text font-medium text-transparent bg-gradient-to-br from-pink-500 via-red-500 to-yellow-500 hover:text-pink-600 hover:bg-none`;
4+
5+
export const LIGHT_COLORS = ["#E9D5FF", "#FBCFE8", "#FECACA", "#FDE68A"];
6+
7+
export const DARK_COLORS = [
8+
"#FB7185",
9+
"#FBBF24",
10+
"#34D399",
11+
"#E879F9",
12+
"#38BDF8",
13+
"#9CA3AF",
14+
"#FB923C",
15+
];

lib/shuffleArray.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Fisher–Yates Shuffle Algorithm
2+
export function shuffleArray(array) {
3+
for (let i = array.length - 1; i > 0; i--) {
4+
let j = Math.floor(Math.random() * (i + 1));
5+
let temp = array[i];
6+
array[i] = array[j];
7+
array[j] = temp;
8+
}
9+
10+
return array;
11+
}

lib/useIsFontReady.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
3+
// a custom hook to detect when custom fonts have finished loading
4+
export function useIsFontReady() {
5+
const [isReady, setIsReady] = React.useState(false);
6+
7+
React.useEffect(() => {
8+
// https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts
9+
document.fonts.ready.then(() => {
10+
setIsReady(true);
11+
});
12+
}, []);
13+
14+
return isReady;
15+
}

next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
domains: [
44
"i.scdn.co", // Spotify Album Art
55
"pbs.twimg.com", // Twitter Profile Picture
6+
"api.microlink.io", // Microlink Image Preview
67
],
78
},
89
webpack: (config, { dev, isServer }) => {

0 commit comments

Comments
 (0)