Skip to content

Commit

Permalink
Changelog-redesign-image-component (#1549)
Browse files Browse the repository at this point in the history
* Add tailwind-variants

* Image component

* Add next/image to introduce lazy loading and get/set img dimensions

* Rm console logs

* address comments

---------

Co-authored-by: Ryan Curtis <[email protected]>
  • Loading branch information
lindseybradford and ryancurtis1 authored Oct 25, 2024
1 parent 268ac74 commit 493a652
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 63 deletions.
70 changes: 17 additions & 53 deletions components/ChangelogIndex.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useState, useEffect } from "react";
import { getPagesUnderRoute } from "nextra/context";
import { ImageFrame } from "./ImageFrame";
import { VideoButtonWithModal } from "./VideoButtonWithModal";
import Link from "next/link";

enum PostFilterOptions {
Expand All @@ -8,54 +10,13 @@ enum PostFilterOptions {
Updates = `updates`,
}

const renderMedia = (page) => {
if (page.frontMatter?.thumbnail) {
return (
<img
src={page.frontMatter.thumbnail}
alt="Thumbnail"
style={{
width: "100%",
borderRadius: "16px",
marginBottom: "16px",
}}
className="max-w-full h-auto"
/>
);
} else if (page.frontMatter?.video) {
const videoURL = page.frontMatter.video;
let embedURL;

if (videoURL.includes("youtube.com") || videoURL.includes("youtu.be")) {
const videoId = videoURL.split("v=")[1]
? videoURL.split("v=")[1].split("&")[0]
: videoURL.split("/").pop();
embedURL = `https://www.youtube.com/embed/${videoId}`;
} else if (videoURL.includes("loom.com")) {
const videoId = videoURL.split("/").pop();
embedURL = `https://www.loom.com/embed/${videoId}?hideEmbedTopBar=true`;
}

return (
<iframe
src={embedURL}
style={{
width: "100%",
aspectRatio: 16 / 9,
height: "auto",
borderRadius: "16px",
marginBottom: "16px",
}}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="Video"
></iframe>
);
}
return "";
const renderImage = (page) => {
return (
<ImageFrame src={page.frontMatter.thumbnail} alt={page.frontMatter.title} />
);
};

export default function ChangelogIndex({ more = "Read more" }) {
export default function ChangelogIndex({ more = "Learn More" }) {
// naturally sorts pages from a-z rather than z-a
const allPages = getPagesUnderRoute("/changelogs").reverse();
const itemsPerPage = 10;
Expand Down Expand Up @@ -133,7 +94,7 @@ export default function ChangelogIndex({ more = "Read more" }) {
</div>

<div className="changelogIndexItemBody">
{renderMedia(page)}
{page.frontMatter?.thumbnail && renderImage(page)}

<h3>
<Link
Expand All @@ -146,13 +107,16 @@ export default function ChangelogIndex({ more = "Read more" }) {
</h3>

<p className="opacity-80 mt-6 leading-7">
{page.frontMatter?.description}{" "}
<span className="inline-block">
<Link href={page.route} className="changelogReadMoreLink">
{more + " →"}
</Link>
</span>
{page.frontMatter?.description}
</p>
<div className="nx-isolate nx-inline-flex nx-items-center nx-space-x-5 nx-mt-8">
{page.frontMatter?.video && (
<VideoButtonWithModal src={page.frontMatter.video} />
)}
<Link href={page.route} className="changelogReadMoreLink">
{more + " →"}
</Link>
</div>
<div className="changelogDivider nx-mt-16"></div>
</div>
</div>
Expand Down
61 changes: 61 additions & 0 deletions components/ImageFrame/ImageFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useLayoutEffect, useRef, useState } from "react";
import Image from "next/image";
// https://www.tailwind-variants.org/docs
import { tv } from "tailwind-variants";

type ImageFrameProps = {
src: string;
alt?: string;
};

const MAX_IMAGE_HEIGHT_WITHOUT_OVERFLOW = 400;

export default function ImageFrame({
alt = "Thumbnail of screenshot",
...props
}: ImageFrameProps) {
const imageRef = useRef<HTMLImageElement>(null);
const [height, setHeight] = useState(0);
const [width, setWidth] = useState(0);

useLayoutEffect(() => {
if (imageRef.current) {
setHeight(imageRef.current.getBoundingClientRect().height);
setWidth(imageRef.current.getBoundingClientRect().width);
}
}, []);

const isTall = height > MAX_IMAGE_HEIGHT_WITHOUT_OVERFLOW;

const imageFrame = tv({
base: "nx-aspect-video nx-overflow-hidden nx-nx-mt-8 lg:nx-rounded-3xl nx-roundex-xl nx-bg-base80 nx-bg-gradient-to-t nx-from-grey20 nx-mb-8 lg:nx-px-14",
variants: {
isTall: {
false: "nx-flex nx-justify-center nx-items-center",
},
},
});

const imageSelf = tv({
base: "nx-w-full max-h-96 h-full nx-shadow-sm",
variants: {
isTall: {
true: "nx-border nx-border-grey20",
false: "nx-rounded-md",
},
},
});

return (
<div className={imageFrame({ isTall: isTall })}>
<Image
ref={imageRef}
src={props.src}
height={height}
width={width}
className={imageSelf({ isTall: isTall })}
alt={alt}
/>
</div>
);
}
1 change: 1 addition & 0 deletions components/ImageFrame/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ImageFrame } from "./ImageFrame";
61 changes: 61 additions & 0 deletions components/VideoButtonWithModal/VideoButtonWithModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from "react";
import { Dialog } from "@headlessui/react";
import getVideoEmbedURL from "./util";
// https://www.tailwind-variants.org/docs
import { tv } from "tailwind-variants";
import VideoIcon from "../svg/VideoIcon";

type VideoButtonModalProps = {
src: string;
title?: string;
};

export default function VideoButtonWithModal({
title = "Video about this feature",
...props
}: VideoButtonModalProps) {
const embedURL = getVideoEmbedURL(props.src);
let [isOpen, setIsOpen] = useState(false);

// TODO: update this style and abstract it as time allows to a single button component
// https://www.figma.com/design/8kiticjQNChvsP9y7s9SRf/Product-Releases-(Copy)?node-id=982-75355&node-type=frame&t=O7vwnwoAoOx42stw-0
const playButton = tv({
base: "nx-flex nx-items-center nx-rounded-full nx-border-2 nx-border-purple140 nx-px-2 nx-py-1 nx-text-xs nx-font-semibold nx-text-purple140 nx-shadow-sm hover:nx-rounded-lg focus-visible:nx-outline focus-visible:nx-outline-2 focus-visible:nx-outline-offset-2 focus-visible:nx-outline-purple140",
});

return (
<>
<button
type="button"
className={playButton()}
onClick={() => setIsOpen(true)}
>
<VideoIcon />
Watch the Video
</button>

<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
<div className="nx-fixed nx-inset-0 nx-flex nx-w-screen nx-items-center nx-justify-center nx-p-4 nx-bg-black nx-bg-opacity-80">
<Dialog.Panel className="nx-w-full nx-max-w-6xl">
<iframe
src={embedURL}
style={{
width: "100%",
aspectRatio: 16 / 9,
height: "auto",
borderRadius: "16px",
}}
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
title={title}
allowFullScreen
></iframe>
</Dialog.Panel>
</div>
</Dialog>
</>
);
}
1 change: 1 addition & 0 deletions components/VideoButtonWithModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as VideoButtonWithModal } from "./VideoButtonWithModal";
30 changes: 30 additions & 0 deletions components/VideoButtonWithModal/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const urlMatchesDomain = (domains: string[], domainStartsWith: string, url: string ) =>
domains
.filter((domain) => domain.startsWith(domainStartsWith))
.some((domain) => url.includes(domain));

const getYoutubeEmbedURL = (url: string) => {
const videoId = url.split("v=")[1]
? url.split("v=")[1].split("&")[0]
: url.split("/").pop();
return `https://www.youtube.com/embed/${videoId}`;
};

const getLoomEmbedURL = (url: string) => {
const videoId = url.split("/").pop();
return `https://www.loom.com/embed/${videoId}?hideEmbedTopBar=true`;
};

const getVideoEmbedURL = (url: string) => {
const domains = ["youtube.com", "youtu.be", "loom.com"];

if (urlMatchesDomain(domains, "youtu", url))
return getYoutubeEmbedURL(url);

if (urlMatchesDomain(domains, "loom", url))
return getLoomEmbedURL(url);

return null;
};

export default getVideoEmbedURL;
126 changes: 126 additions & 0 deletions components/svg/VideoIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
export default function VideoIcon() {
return (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_di_1120_87272)">
<circle
cx="13.8027"
cy="12.6547"
r="10"
fill="#7856FF"
shape-rendering="crispEdges"
/>
<circle
cx="13.8027"
cy="12.6547"
r="10.0962"
stroke="white"
stroke-opacity="0.1"
stroke-width="0.192307"
shape-rendering="crispEdges"
/>
</g>
<g filter="url(#filter1_i_1120_87272)">
<path
d="M17.5305 12.2981C17.8523 12.4273 17.8523 12.8829 17.5305 13.012L11.0632 15.6075C10.8105 15.7089 10.5353 15.5229 10.5353 15.2506L10.5353 10.0595C10.5353 9.78722 10.8105 9.60116 11.0632 9.70257L17.5305 12.2981Z"
fill="#E8DDFF"
/>
</g>
<defs>
<filter
id="filter0_di_1120_87272"
x="0.277018"
y="0.795736"
width="27.0514"
height="27.0512"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="1.66667" />
<feGaussianBlur stdDeviation="1.66667" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_1120_87272"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_1120_87272"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="-1.66667" />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.313726 0 0 0 0 0.156863 0 0 0 0 0.752941 0 0 0 0.4 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect2_innerShadow_1120_87272"
/>
</filter>
<filter
id="filter1_i_1120_87272"
x="10.5352"
y="8.90533"
width="7.23682"
height="6.73029"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="-0.769229" />
<feGaussianBlur stdDeviation="0.384614" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.690196 0 0 0 0 0.580392 0 0 0 0 1 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_1120_87272"
/>
</filter>
</defs>
</svg>
);
}
Loading

0 comments on commit 493a652

Please sign in to comment.