Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changelog project #1572

Merged
merged 28 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ce8a94a
[Changelog] add new header to main changelog page (#1551)
ryancurtis1 Oct 21, 2024
268ac74
[Changelog] add filter bar to changelog page (#1558)
ryancurtis1 Oct 25, 2024
493a652
Changelog-redesign-image-component (#1549)
lindseybradford Oct 25, 2024
1fd14a5
[Changelog] post page updates to use new formatting (#1574)
ryancurtis1 Oct 28, 2024
6adab3d
[Changelog] add subscribe to product updates modal and functionality …
ryancurtis1 Nov 4, 2024
da89ce1
[Changelog] update formatting for past 10 posts to match new styling …
ryancurtis1 Nov 5, 2024
8a5609b
small changes to formatting for images and ctas on announcement posts…
ryancurtis1 Nov 6, 2024
fe3df02
[Changelog] add dark mode support for changelog pages (#1596)
ryancurtis1 Nov 6, 2024
2a7aa20
[ChangeLog] Light and Dark mode filter button states (#1604)
junhouse Nov 8, 2024
d01318e
[Changelog] Watch Video button states (#1605)
junhouse Nov 8, 2024
3c2c86c
[ChangeLog] Add states to Watch the video button in post (#1606)
junhouse Nov 12, 2024
75eeee1
[ChangeLog] More button States (#1610)
junhouse Nov 12, 2024
45fa350
update load more logic (#1611)
junhouse Nov 13, 2024
c2f6b56
scss update (#1615)
junhouse Nov 14, 2024
f16b9e6
new image and class behavior (#1618)
junhouse Nov 14, 2024
8e140ac
[Changelog] fix pagination for changelog posts (#1619)
ryancurtis1 Nov 14, 2024
3159b8d
[Changelog] Header img css update (#1620)
junhouse Nov 14, 2024
fe105b4
updating styling for background (#1621)
ryancurtis1 Nov 14, 2024
0bb66e5
add images for announcement posts (#1623)
ryancurtis1 Nov 14, 2024
3b9ed64
[changelog] bug fixes for small screens (#1625)
ryancurtis1 Nov 14, 2024
8c36686
merge
ryancurtis1 Nov 18, 2024
945145c
[ChangeLog] Different image background (#1624)
junhouse Nov 19, 2024
5cf2870
show video in feed if post doesn't have image (#1627)
ryancurtis1 Nov 19, 2024
d3fcbd5
small styling updates and addressing comments (#1628)
ryancurtis1 Nov 19, 2024
b930757
Fix code scanning alert no. 18: Incomplete URL substring sanitization
ryancurtis1 Nov 19, 2024
d580825
Fix code scanning alert no. 16: Incomplete URL substring sanitization
ryancurtis1 Nov 19, 2024
944b6ef
Merge branch 'main' into changelog-project
ryancurtis1 Nov 20, 2024
7d9bd48
fix type error
ryancurtis1 Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions components/ChangelogIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import React, { useState, useEffect } from "react";
import { getPagesUnderRoute } from "nextra/context";
import { ImageFrame } from "./ImageFrame";
import { VideoButtonWithModal } from "./VideoButtonWithModal";
import Link from "next/link";
import { MdxFile } from "nextra";

enum PostFilterOptions {
All = `all`,
Announcements = `announcements`,
Updates = `updates`,
}

const renderImage = (page) => {
return (
<ImageFrame
src={page.frontMatter.thumbnail}
alt={page.frontMatter.title}
isAnnouncement={page.frontMatter?.isAnnouncement}
/>
);
};

const getVideoEmbedURL = (videoURL) => {
try {
const parsedURL = new URL(videoURL);
const host = parsedURL.host;
if (host === "www.youtube.com" || host === "youtube.com" || host === "youtu.be") {
const videoId = parsedURL.searchParams.get("v") || parsedURL.pathname.split("/").pop();
return `https://www.youtube.com/embed/${videoId}`;
} else if (host === "www.loom.com" || host === "loom.com") {
const videoId = parsedURL.pathname.split("/").pop();
return `https://www.loom.com/embed/${videoId}?hideEmbedTopBar=true`;
}
} catch (e) {
console.error("Invalid URL:", e);
}
};

const renderVideo = (videoURL) => {
const embedURL = getVideoEmbedURL(videoURL);

return (
<iframe
src={embedURL}
style={{
width: "100%",
aspectRatio: 16 / 9,
height: "auto",
borderRadius: "16px",
marginBottom: "16px",
}}
allow="clipboard-write; encrypted-media; picture-in-picture"
allowFullScreen
title="Video"
></iframe>
);
};

const renderMedia: (page: MdxFile) => React.JSX.Element = (page) => {
if (page.frontMatter?.thumbnail) {
return renderImage(page);
} else if (page.frontMatter?.video) {
return renderVideo(page.frontMatter?.video);
}
return null;
};

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;
const [displayedPages, setDisplayedPages] = useState([]);
const [pageIndex, setPageIndex] = useState(0);
const [filter, setFilter] = useState(PostFilterOptions.All);

const getAllFilteredPages = () => {
return allPages.filter((page) => {
switch (filter) {
case PostFilterOptions.Updates:
// @ts-ignore:next-line
return !page.frontMatter?.isAnnouncement;
case PostFilterOptions.Announcements:
// @ts-ignore:next-line
return page.frontMatter?.isAnnouncement === true;
default:
return true;
}
});
};

// Load initial or additional pages
useEffect(() => {
const morePages = getAllFilteredPages().slice(0, pageIndex + itemsPerPage);

setDisplayedPages(morePages);
}, [pageIndex, filter]);

const loadMore = () => {
setPageIndex((prev) => prev + itemsPerPage);
};

const filterButton = (id: PostFilterOptions, label: string) => {
let className = "changelogFilterButton";
if (filter === id) {
className += " active";
}
return (
<button
className={className}
onClick={() => {
setFilter(id);
setPageIndex(0);
}}
>
{label}
</button>
);
};

const filterOptions = [
{ id: PostFilterOptions.All, label: "All Posts" },
{ id: PostFilterOptions.Announcements, label: "Announcements" },
{ id: PostFilterOptions.Updates, label: "Updates" },
];

return (
<div
style={{
display: "flex",
alignItems: "center",
flexDirection: "column",
}}
className="changelogIndexContainer"
>
<div className="changelogIndexFilterBorder" />
<div className="changelogIndexFilterBar">
{filterOptions.map((filter) => filterButton(filter.id, filter.label))}
</div>
<div className="changelogIndexFilterBorder changelogIndexFilterBorderBottom" />

{displayedPages.map((page) => (
<div key={page.route} className="changelogIndexItem nx-mt-16">
<div className="changelogIndexItemDate">
{page.frontMatter?.date ? (
<p className="changelogDate">
{new Date(page.frontMatter.date).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
) : null}
</div>

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

<h3
className={
page.frontMatter?.thumbnail && "changelogItemTitleWrapper"
}
>
<Link
href={page.route}
style={{ color: "inherit", textDecoration: "none" }}
className="changelogItemTitle block"
>
{page.meta?.title || page.frontMatter?.title || page.name}
</Link>
</h3>

<p className="opacity-80 mt-6 leading-7">
{page.frontMatter?.description}
</p>
<div className="nx-isolate nx-inline-flex nx-items-center nx-space-x-5 nx-mt-8">
{page.frontMatter?.isAnnouncement && (
<a
href="https://mixpanel.com/contact-us/demo-request/"
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"
>
Request a Demo
</a>
)}
{page.frontMatter?.video && page.frontMatter?.thumbnail && (
<VideoButtonWithModal
src={page.frontMatter.video}
showThumbnail={false}
/>
)}
<Link
target="_blank"
href={page.route}
className="changelogReadMoreLink"
>
{more + " →"}
</Link>
</div>
<div className="changelogDivider nx-mt-16"></div>
</div>
</div>
))}
{pageIndex + itemsPerPage < getAllFilteredPages().length && (
<div className="changelogLoadMoreButtonContainer">
<button onClick={loadMore} className="changelogLoadMoreButton">
Load More
</button>
</div>
)}
</div>
);
}
31 changes: 31 additions & 0 deletions components/ChangelogPostHeader/ChangelogPostHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ImageFrame } from "../ImageFrame";

type ChangelogHeaderProps = {
date?: string;
image?: any;
title?: string;
};

export default function ChangelogPostHeader({
date,
image,
title,
}: ChangelogHeaderProps) {
return (
<div className="changelogPostHeader">
{date && (
<p className="changelogDate">
{new Date(date).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
)}

{title && <h3 className="changelogTitle">{title}</h3>}

{image && <ImageFrame src={image} alt={title} />}
</div>
);
}
102 changes: 102 additions & 0 deletions components/ImageFrame/ImageFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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;
isAnnouncement?: boolean;
};

const MAX_IMAGE_HEIGHT_WITHOUT_OVERFLOW = 400;

export default function ImageFrame({
alt = "Thumbnail of screenshot",
isAnnouncement = false,
...props
}: ImageFrameProps) {
const imageRef = useRef<HTMLImageElement>(null);
const lightImageRef = useRef<HTMLImageElement>(null);
const darkImageRef = useRef<HTMLImageElement>(null);

const [height, setHeight] = useState(0);
const [width, setWidth] = useState(0);

useLayoutEffect(() => {
if (isAnnouncement) {
setHeight(imageRef.current.getBoundingClientRect().height);
setWidth(imageRef.current.getBoundingClientRect().width);
} else {
// display: none still renders the HTML, and returns 0 for both height and width
setHeight(
Math.max(
lightImageRef.current?.getBoundingClientRect().height,
darkImageRef.current?.getBoundingClientRect().height
)
);
setWidth(
Math.max(
lightImageRef?.current?.getBoundingClientRect().width,
darkImageRef?.current?.getBoundingClientRect().width
)
);
}
}, [setHeight, setWidth]);

const isTall = height > MAX_IMAGE_HEIGHT_WITHOUT_OVERFLOW;

const createTVConfig = (is_light = true) => {
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`;
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`;
return {
base: is_light ? lightBase : darkBase,
variants: {
isTall: {
false: "nx-flex nx-justify-center nx-items-center",
},
},
};
};

const darkImageFrame = tv(createTVConfig(false));
const lightImageFrame = tv(createTVConfig(true));

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-2xl",
},
isAnnouncement: {
true: "lg:nx-rounded-3xl nx-rounded-xl nx-mb-8 nx-bg-transparent nx-border-0",
},
},
});

const renderImageComponent = (refInstance) => (
<Image
ref={refInstance}
src={props.src}
height={height}
width={width}
className={imageSelf({ isTall: isTall, isAnnouncement: isAnnouncement })}
alt={alt}
/>
);

return isAnnouncement ? (
<>{renderImageComponent(imageRef)}</>
) : (
<>
<div className={`lightImageFrame ` + lightImageFrame({ isTall: isTall })}>
{renderImageComponent(lightImageRef)}
</div>

<div className={`darkImageFrame ` + darkImageFrame({ isTall: isTall })}>
{renderImageComponent(darkImageRef)}
</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";
8 changes: 4 additions & 4 deletions components/MainContent/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { FeedbackCollector } from "../FeedbackCollector/FeedbackCollector";

interface Props {
children: React.ReactNode
children: React.ReactNode;
}

const MainContent: React.FC<Props> = ({children}): JSX.Element => {
const MainContent: React.FC<Props> = ({ children }): JSX.Element => {
return (
<>
{children}
<FeedbackCollector />
</>
)
}
);
};

export default MainContent;
4 changes: 2 additions & 2 deletions components/SignUpButton/SignUpButton.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

.signUpButton {
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
background-color: colors.$purple50;
font-weight: 600;
background: linear-gradient(1deg, colors.$purple100 0.97%, colors.$purple50 96.96%);
font-weight: 500;
font-size: 1rem;
line-height: 1.5rem;
color: colors.$white;
Expand Down
Loading