Skip to content

Commit 1563391

Browse files
ryancurtis1lindseybradfordjunhousegithub-advanced-security[bot]
authored
Changelog project (#1572)
* [Changelog] add new header to main changelog page (#1551) * add new header to main changelog page * [Changelog] update layout and formatting of changelog feed (#1552) * update layout of changelog feed * add styling for small screens * add styling for extra large * [Changelog] add filter bar to changelog page (#1558) * add new header to main changelog page * update layout of changelog feed * add styling for small screens * [Changelog] update layout and formatting of changelog feed (#1552) * update layout of changelog feed * add styling for small screens * save post filters * add styling for extra large * add filtering of posts by announcement or update * remove console log * rename as tsx * address comments * address comments * Changelog-redesign-image-component (#1549) * 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]> * [Changelog] post page updates to use new formatting (#1574) * update page layout * add header component * add new components to post page ' * update video button styling * [Changelog] add subscribe to product updates modal and functionality (#1582) * add subscribe form api * more styling for subscribe modal * add reporting to rollbar for subscribe errors * [Changelog] update formatting for past 10 posts to match new styling (#1575) * update page layout * add header component * add new components to post page ' * update video button styling * update formatting for old posts * small changes to formatting for images and ctas on announcement posts (#1597) * [Changelog] add dark mode support for changelog pages (#1596) * add dark mode support for changelog pages * address comments * [ChangeLog] Light and Dark mode filter button states (#1604) * light mode * remove black background * add dark mode * sort * [Changelog] Watch Video button states (#1605) * light and dark mode * add dark video icon * add dark icon * [ChangeLog] Add states to Watch the video button in post (#1606) * add styling * light style * pr feedback * [ChangeLog] More button States (#1610) * add styling * wip * update load more * clean up * remove whitepsace * update load more logic (#1611) * scss update (#1615) * new image and class behavior (#1618) * [Changelog] fix pagination for changelog posts (#1619) * [Changelog] Header img css update (#1620) * align to top * scale * updating styling for background (#1621) * add images for announcement posts (#1623) * [changelog] bug fixes for small screens (#1625) * add images for announcement posts * update the image frame and post title styles for small screens * [ChangeLog] Different image background (#1624) * light and dark gradient background * clean up * clean up * fix background opacity * fix nx naming * extract color * move css to parent level * show video in feed if post doesn't have image (#1627) * small styling updates and addressing comments (#1628) * Fix code scanning alert no. 18: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix code scanning alert no. 16: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix type error --------- Co-authored-by: Lindsey Bradford <[email protected]> Co-authored-by: KyungJun Kim <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 0f68974 commit 1563391

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1886
-277
lines changed

components/ChangelogIndex.tsx

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import React, { useState, useEffect } from "react";
2+
import { getPagesUnderRoute } from "nextra/context";
3+
import { ImageFrame } from "./ImageFrame";
4+
import { VideoButtonWithModal } from "./VideoButtonWithModal";
5+
import Link from "next/link";
6+
import { MdxFile } from "nextra";
7+
8+
enum PostFilterOptions {
9+
All = `all`,
10+
Announcements = `announcements`,
11+
Updates = `updates`,
12+
}
13+
14+
const renderImage = (page) => {
15+
return (
16+
<ImageFrame
17+
src={page.frontMatter.thumbnail}
18+
alt={page.frontMatter.title}
19+
isAnnouncement={page.frontMatter?.isAnnouncement}
20+
/>
21+
);
22+
};
23+
24+
const getVideoEmbedURL = (videoURL) => {
25+
try {
26+
const parsedURL = new URL(videoURL);
27+
const host = parsedURL.host;
28+
if (host === "www.youtube.com" || host === "youtube.com" || host === "youtu.be") {
29+
const videoId = parsedURL.searchParams.get("v") || parsedURL.pathname.split("/").pop();
30+
return `https://www.youtube.com/embed/${videoId}`;
31+
} else if (host === "www.loom.com" || host === "loom.com") {
32+
const videoId = parsedURL.pathname.split("/").pop();
33+
return `https://www.loom.com/embed/${videoId}?hideEmbedTopBar=true`;
34+
}
35+
} catch (e) {
36+
console.error("Invalid URL:", e);
37+
}
38+
};
39+
40+
const renderVideo = (videoURL) => {
41+
const embedURL = getVideoEmbedURL(videoURL);
42+
43+
return (
44+
<iframe
45+
src={embedURL}
46+
style={{
47+
width: "100%",
48+
aspectRatio: 16 / 9,
49+
height: "auto",
50+
borderRadius: "16px",
51+
marginBottom: "16px",
52+
}}
53+
allow="clipboard-write; encrypted-media; picture-in-picture"
54+
allowFullScreen
55+
title="Video"
56+
></iframe>
57+
);
58+
};
59+
60+
const renderMedia: (page: MdxFile) => React.JSX.Element = (page) => {
61+
if (page.frontMatter?.thumbnail) {
62+
return renderImage(page);
63+
} else if (page.frontMatter?.video) {
64+
return renderVideo(page.frontMatter?.video);
65+
}
66+
return null;
67+
};
68+
69+
export default function ChangelogIndex({ more = "Learn More" }) {
70+
// naturally sorts pages from a-z rather than z-a
71+
const allPages = getPagesUnderRoute("/changelogs").reverse();
72+
const itemsPerPage = 10;
73+
const [displayedPages, setDisplayedPages] = useState([]);
74+
const [pageIndex, setPageIndex] = useState(0);
75+
const [filter, setFilter] = useState(PostFilterOptions.All);
76+
77+
const getAllFilteredPages = () => {
78+
return allPages.filter((page) => {
79+
switch (filter) {
80+
case PostFilterOptions.Updates:
81+
// @ts-ignore:next-line
82+
return !page.frontMatter?.isAnnouncement;
83+
case PostFilterOptions.Announcements:
84+
// @ts-ignore:next-line
85+
return page.frontMatter?.isAnnouncement === true;
86+
default:
87+
return true;
88+
}
89+
});
90+
};
91+
92+
// Load initial or additional pages
93+
useEffect(() => {
94+
const morePages = getAllFilteredPages().slice(0, pageIndex + itemsPerPage);
95+
96+
setDisplayedPages(morePages);
97+
}, [pageIndex, filter]);
98+
99+
const loadMore = () => {
100+
setPageIndex((prev) => prev + itemsPerPage);
101+
};
102+
103+
const filterButton = (id: PostFilterOptions, label: string) => {
104+
let className = "changelogFilterButton";
105+
if (filter === id) {
106+
className += " active";
107+
}
108+
return (
109+
<button
110+
className={className}
111+
onClick={() => {
112+
setFilter(id);
113+
setPageIndex(0);
114+
}}
115+
>
116+
{label}
117+
</button>
118+
);
119+
};
120+
121+
const filterOptions = [
122+
{ id: PostFilterOptions.All, label: "All Posts" },
123+
{ id: PostFilterOptions.Announcements, label: "Announcements" },
124+
{ id: PostFilterOptions.Updates, label: "Updates" },
125+
];
126+
127+
return (
128+
<div
129+
style={{
130+
display: "flex",
131+
alignItems: "center",
132+
flexDirection: "column",
133+
}}
134+
className="changelogIndexContainer"
135+
>
136+
<div className="changelogIndexFilterBorder" />
137+
<div className="changelogIndexFilterBar">
138+
{filterOptions.map((filter) => filterButton(filter.id, filter.label))}
139+
</div>
140+
<div className="changelogIndexFilterBorder changelogIndexFilterBorderBottom" />
141+
142+
{displayedPages.map((page) => (
143+
<div key={page.route} className="changelogIndexItem nx-mt-16">
144+
<div className="changelogIndexItemDate">
145+
{page.frontMatter?.date ? (
146+
<p className="changelogDate">
147+
{new Date(page.frontMatter.date).toLocaleDateString(undefined, {
148+
year: "numeric",
149+
month: "long",
150+
day: "numeric",
151+
})}
152+
</p>
153+
) : null}
154+
</div>
155+
156+
<div className="changelogIndexItemBody">
157+
{(page.frontMatter?.thumbnail || page.frontMatter?.video) &&
158+
renderMedia(page)}
159+
160+
<h3
161+
className={
162+
page.frontMatter?.thumbnail && "changelogItemTitleWrapper"
163+
}
164+
>
165+
<Link
166+
href={page.route}
167+
style={{ color: "inherit", textDecoration: "none" }}
168+
className="changelogItemTitle block"
169+
>
170+
{page.meta?.title || page.frontMatter?.title || page.name}
171+
</Link>
172+
</h3>
173+
174+
<p className="opacity-80 mt-6 leading-7">
175+
{page.frontMatter?.description}
176+
</p>
177+
<div className="nx-isolate nx-inline-flex nx-items-center nx-space-x-5 nx-mt-8">
178+
{page.frontMatter?.isAnnouncement && (
179+
<a
180+
href="https://mixpanel.com/contact-us/demo-request/"
181+
className="nx-px-5 nx-py-3 nx-my-4 nx-drop-shadow-sm nx-bg-gradient-to-t nx-from-purple100 nx-to-purple50 nx-rounded-full nx-text-white nx-font-medium nx-text-medium"
182+
>
183+
Request a Demo
184+
</a>
185+
)}
186+
{page.frontMatter?.video && page.frontMatter?.thumbnail && (
187+
<VideoButtonWithModal
188+
src={page.frontMatter.video}
189+
showThumbnail={false}
190+
/>
191+
)}
192+
<Link
193+
target="_blank"
194+
href={page.route}
195+
className="changelogReadMoreLink"
196+
>
197+
{more + " →"}
198+
</Link>
199+
</div>
200+
<div className="changelogDivider nx-mt-16"></div>
201+
</div>
202+
</div>
203+
))}
204+
{pageIndex + itemsPerPage < getAllFilteredPages().length && (
205+
<div className="changelogLoadMoreButtonContainer">
206+
<button onClick={loadMore} className="changelogLoadMoreButton">
207+
Load More
208+
</button>
209+
</div>
210+
)}
211+
</div>
212+
);
213+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ImageFrame } from "../ImageFrame";
2+
3+
type ChangelogHeaderProps = {
4+
date?: string;
5+
image?: any;
6+
title?: string;
7+
};
8+
9+
export default function ChangelogPostHeader({
10+
date,
11+
image,
12+
title,
13+
}: ChangelogHeaderProps) {
14+
return (
15+
<div className="changelogPostHeader">
16+
{date && (
17+
<p className="changelogDate">
18+
{new Date(date).toLocaleDateString(undefined, {
19+
year: "numeric",
20+
month: "long",
21+
day: "numeric",
22+
})}
23+
</p>
24+
)}
25+
26+
{title && <h3 className="changelogTitle">{title}</h3>}
27+
28+
{image && <ImageFrame src={image} alt={title} />}
29+
</div>
30+
);
31+
}

components/ImageFrame/ImageFrame.tsx

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { useLayoutEffect, useRef, useState } from "react";
2+
import Image from "next/image";
3+
// https://www.tailwind-variants.org/docs
4+
import { tv } from "tailwind-variants";
5+
6+
type ImageFrameProps = {
7+
src: string;
8+
alt?: string;
9+
isAnnouncement?: boolean;
10+
};
11+
12+
const MAX_IMAGE_HEIGHT_WITHOUT_OVERFLOW = 400;
13+
14+
export default function ImageFrame({
15+
alt = "Thumbnail of screenshot",
16+
isAnnouncement = false,
17+
...props
18+
}: ImageFrameProps) {
19+
const imageRef = useRef<HTMLImageElement>(null);
20+
const lightImageRef = useRef<HTMLImageElement>(null);
21+
const darkImageRef = useRef<HTMLImageElement>(null);
22+
23+
const [height, setHeight] = useState(0);
24+
const [width, setWidth] = useState(0);
25+
26+
useLayoutEffect(() => {
27+
if (isAnnouncement) {
28+
setHeight(imageRef.current.getBoundingClientRect().height);
29+
setWidth(imageRef.current.getBoundingClientRect().width);
30+
} else {
31+
// display: none still renders the HTML, and returns 0 for both height and width
32+
setHeight(
33+
Math.max(
34+
lightImageRef.current?.getBoundingClientRect().height,
35+
darkImageRef.current?.getBoundingClientRect().height
36+
)
37+
);
38+
setWidth(
39+
Math.max(
40+
lightImageRef?.current?.getBoundingClientRect().width,
41+
darkImageRef?.current?.getBoundingClientRect().width
42+
)
43+
);
44+
}
45+
}, [setHeight, setWidth]);
46+
47+
const isTall = height > MAX_IMAGE_HEIGHT_WITHOUT_OVERFLOW;
48+
49+
const createTVConfig = (is_light = true) => {
50+
const lightBase = `nx-aspect-video nx-overflow-hidden nx-nx-mt-8 lg:nx-rounded-3xl nx-rounded-xl nx-mb-8 nx-px-8 sm:nx-px-10 md:nx-px-10 lg:nx-px-14 nx-bg-gradient-to-t nx-from-grey20-opacity-100 nx-to-grey20-opacity-50`;
51+
const darkBase = `nx-aspect-video nx-overflow-hidden nx-nx-mt-8 lg:nx-rounded-3xl nx-rounded-xl nx-mb-8 nx-px-8 sm:nx-px-10 md:nx-px-10 lg:nx-px-14 nx-bg-gradient-to-t nx-from-grey20-opacity-8 nx-to-grey20-opacity-5`;
52+
return {
53+
base: is_light ? lightBase : darkBase,
54+
variants: {
55+
isTall: {
56+
false: "nx-flex nx-justify-center nx-items-center",
57+
},
58+
},
59+
};
60+
};
61+
62+
const darkImageFrame = tv(createTVConfig(false));
63+
const lightImageFrame = tv(createTVConfig(true));
64+
65+
const imageSelf = tv({
66+
base: "nx-w-full max-h-96 h-full nx-shadow-sm",
67+
variants: {
68+
isTall: {
69+
true: "nx-border nx-border-grey20",
70+
false: "nx-rounded-2xl",
71+
},
72+
isAnnouncement: {
73+
true: "lg:nx-rounded-3xl nx-rounded-xl nx-mb-8 nx-bg-transparent nx-border-0",
74+
},
75+
},
76+
});
77+
78+
const renderImageComponent = (refInstance) => (
79+
<Image
80+
ref={refInstance}
81+
src={props.src}
82+
height={height}
83+
width={width}
84+
className={imageSelf({ isTall: isTall, isAnnouncement: isAnnouncement })}
85+
alt={alt}
86+
/>
87+
);
88+
89+
return isAnnouncement ? (
90+
<>{renderImageComponent(imageRef)}</>
91+
) : (
92+
<>
93+
<div className={`lightImageFrame ` + lightImageFrame({ isTall: isTall })}>
94+
{renderImageComponent(lightImageRef)}
95+
</div>
96+
97+
<div className={`darkImageFrame ` + darkImageFrame({ isTall: isTall })}>
98+
{renderImageComponent(darkImageRef)}
99+
</div>
100+
</>
101+
);
102+
}

components/ImageFrame/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ImageFrame } from "./ImageFrame";
+4-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { FeedbackCollector } from "../FeedbackCollector/FeedbackCollector";
22

33
interface Props {
4-
children: React.ReactNode
4+
children: React.ReactNode;
55
}
66

7-
const MainContent: React.FC<Props> = ({children}): JSX.Element => {
7+
const MainContent: React.FC<Props> = ({ children }): JSX.Element => {
88
return (
99
<>
1010
{children}
1111
<FeedbackCollector />
1212
</>
13-
)
14-
}
13+
);
14+
};
1515

1616
export default MainContent;

components/SignUpButton/SignUpButton.module.scss

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
.signUpButton {
44
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
5-
background-color: colors.$purple50;
6-
font-weight: 600;
5+
background: linear-gradient(1deg, colors.$purple100 0.97%, colors.$purple50 96.96%);
6+
font-weight: 500;
77
font-size: 1rem;
88
line-height: 1.5rem;
99
color: colors.$white;

0 commit comments

Comments
 (0)