Skip to content

Commit

Permalink
Add Link Preview and Rough Notation
Browse files Browse the repository at this point in the history
  • Loading branch information
manuarora700 committed Jun 15, 2021
1 parent a0166b7 commit 8b906e9
Show file tree
Hide file tree
Showing 11 changed files with 709 additions and 18 deletions.
115 changes: 115 additions & 0 deletions components/LinkPreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { FOCUS_VISIBLE_OUTLINE, GRADIENT_LINK } from "@/lib/constants";
import { Portal, Transition } from "@headlessui/react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import cx from "clsx";
import Image from "next/image";
import { encode } from "qss";
import React from "react";

export const LinkPreview = ({ children, url }) => {
const width = 200;
const height = 125;
const quality = 50;
const layout = "fixed";

// Simplifies things by encoding our microlink params into a query string.
const params = encode({
url,
screenshot: true,
meta: false,
embed: "screenshot.url",
colorScheme: "dark",
"viewport.isMobile": true,
"viewport.deviceScaleFactor": 1,

// To capture useful content, the screenshot viewport needs to be bigger
// than our images but maintain the same ratio
"viewport.width": width * 3,
"viewport.height": height * 3,
});

const src = `https://api.microlink.io/?${params}`;

const [isOpen, setOpen] = React.useState(false);
// const [static, setStatic] = useState(false);

// if (staticImage) setStatic(true);

const [isMounted, setIsMounted] = React.useState(false);

React.useEffect(() => {
setIsMounted(true);
}, []);

return (
<>
{/**
* Microlink.io + next/image can take a few seconds to fetch and generate
* a screenshot. The delay makes <LinkPreview> pointless. As a hacky
* solution we create a second <Image> in a Portal after the component has
* mounted. This <Image> triggers microlink.io + next/image so that the
* image itself is ready by the time the user hovers on a <LinkPreview>.
* Not concerned about the performance impact because <Image>'s are cached
* after they are generated and the images themselves are tiny (< 10kb).
*/}
{isMounted ? (
<Portal>
<div className="hidden">
<Image
src={src}
width={width}
height={height}
quality={quality}
layout={layout}
priority={true}
/>
</div>
</Portal>
) : null}

<HoverCardPrimitive.Root
openDelay={50}
onOpenChange={(open) => {
setOpen(open);
}}
>
<HoverCardPrimitive.Trigger
href={url}
className={cx(GRADIENT_LINK, FOCUS_VISIBLE_OUTLINE)}
>
{children}
</HoverCardPrimitive.Trigger>

<HoverCardPrimitive.Content side="top" align="center" sideOffset={10}>
<Transition
show={isOpen}
appear={true}
enter="transform transition duration-300 origin-bottom ease-out"
enterFrom="opacity-0 translate-y-2 scale-0"
enterTo="opacity-100 translate-y-0 scale-100"
className="shadow-xl rounded-xl"
>
<a
href={url}
className="block p-1 bg-white border border-transparent shadow rounded-xl hover:border-pink-500"
// Unfortunate hack to remove the weird whitespace left by
// next/image wrapper div
// https://github.com/vercel/next.js/issues/18915
style={{ fontSize: 0 }}
>
<Image
src={src}
width={width}
height={height}
quality={quality}
layout={layout}
priority={true}
className="rounded-lg"
/>
</a>
</Transition>
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Root>
</>
);
};
114 changes: 114 additions & 0 deletions components/StaticLinkPreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { FOCUS_VISIBLE_OUTLINE, GRADIENT_LINK } from "@/lib/constants";
import { Portal, Transition } from "@headlessui/react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import cx from "clsx";
import Image from "next/image";
import { encode } from "qss";
import React from "react";

export const StaticLinkPreview = ({ children, url }) => {
const width = 200;
const height = 125;
const quality = 50;
const layout = "fixed";

// Simplifies things by encoding our microlink params into a query string.
const params = encode({
url,
screenshot: true,
meta: false,
embed: "screenshot.url",
colorScheme: "dark",
"viewport.isMobile": true,
"viewport.deviceScaleFactor": 1,

// To capture useful content, the screenshot viewport needs to be bigger
// than our images but maintain the same ratio
"viewport.width": width * 3,
"viewport.height": height * 3,
});

const src = `https://api.microlink.io/?${params}`;

const [isOpen, setOpen] = React.useState(false);
// const [static, setStatic] = useState(false);

// if (staticImage) setStatic(true);

const [isMounted, setIsMounted] = React.useState(false);

React.useEffect(() => {
setIsMounted(true);
}, []);

return (
<>
{/**
* Microlink.io + next/image can take a few seconds to fetch and generate
* a screenshot. The delay makes <LinkPreview> pointless. As a hacky
* solution we create a second <Image> in a Portal after the component has
* mounted. This <Image> triggers microlink.io + next/image so that the
* image itself is ready by the time the user hovers on a <LinkPreview>.
* Not concerned about the performance impact because <Image>'s are cached
* after they are generated and the images themselves are tiny (< 10kb).
*/}
{isMounted ? (
<Portal>
<div className="hidden">
<Image
src={src}
width={width}
height={height}
quality={quality}
layout={layout}
priority={true}
/>
</div>
</Portal>
) : null}

<HoverCardPrimitive.Root
openDelay={50}
onOpenChange={(open) => {
setOpen(open);
}}
>
<HoverCardPrimitive.Trigger
href={url}
className={cx(GRADIENT_LINK, FOCUS_VISIBLE_OUTLINE)}
>
{children}
</HoverCardPrimitive.Trigger>

<HoverCardPrimitive.Content side="top" align="center" sideOffset={10}>
<Transition
show={isOpen}
appear={true}
enter="transform transition duration-300 origin-bottom ease-out"
enterFrom="opacity-0 translate-y-2 scale-0"
enterTo="opacity-100 translate-y-0 scale-100"
className="shadow-xl rounded-xl"
>
<span
className="block p-1 bg-white border border-transparent shadow rounded-xl hover:border-pink-500"
// Unfortunate hack to remove the weird whitespace left by
// next/image wrapper div
// https://github.com/vercel/next.js/issues/18915
style={{ fontSize: 0 }}
>
<Image
src={src}
width={width}
height={height}
quality={quality}
layout={layout}
priority={true}
className="rounded-lg"
/>
</span>
</Transition>
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Root>
</>
);
};
3 changes: 2 additions & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"@/data/*": ["data/*"],
"@/layouts/*": ["layouts/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
"@/styles/*": ["styles/*"],
"@/ui/*": ["ui/*"],
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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`;

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`;

export const LIGHT_COLORS = ["#E9D5FF", "#FBCFE8", "#FECACA", "#FDE68A"];

export const DARK_COLORS = [
"#FB7185",
"#FBBF24",
"#34D399",
"#E879F9",
"#38BDF8",
"#9CA3AF",
"#FB923C",
];
11 changes: 11 additions & 0 deletions lib/shuffleArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Fisher–Yates Shuffle Algorithm
export function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}

return array;
}
15 changes: 15 additions & 0 deletions lib/useIsFontReady.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";

// a custom hook to detect when custom fonts have finished loading
export function useIsFontReady() {
const [isReady, setIsReady] = React.useState(false);

React.useEffect(() => {
// https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts
document.fonts.ready.then(() => {
setIsReady(true);
});
}, []);

return isReady;
}
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
domains: [
"i.scdn.co", // Spotify Album Art
"pbs.twimg.com", // Twitter Profile Picture
"api.microlink.io", // Microlink Image Preview
],
},
webpack: (config, { dev, isServer }) => {
Expand Down
Loading

0 comments on commit 8b906e9

Please sign in to comment.