Skip to content

Commit

Permalink
new showcase page (#647)
Browse files Browse the repository at this point in the history
* feat(showcases): add case studies to showcase page

* fix(build): ts errors in case study type

* feat(showcases): retrieve showcases in getStaticProps

* feat(showcases): remove download image logic

* feat(showcases): use vercel blob storage for the showcase images

* chore(build): add notion and vercel env vars to turbo config

* feat(showcases): add revalidation route

* feat(svelte-docs): add revalidate route for showcase

* feat(showcase): improve dx when api secrets are not present

* fix(showcase): prevent page freezing in dev mode on hot reload

* feat(showcases): use folder in vercel blob storage for showcase images

* feat(showcase): style case study previews

* refactor(showcases): chnge copy text, sorting and case study preview
  • Loading branch information
chrtze authored Jan 28, 2025
1 parent 0a41f4f commit 8950591
Show file tree
Hide file tree
Showing 126 changed files with 586 additions and 308 deletions.
1 change: 1 addition & 0 deletions packages/xy-shared/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
10 changes: 9 additions & 1 deletion packages/xy-shared/layouts/case-study.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Container,
ContentGrid,
ContentGridItem,
Button,
} from '@xyflow/xy-ui';

import {
Expand All @@ -31,6 +32,7 @@ export type CaseStudyFrontmatter = {
image: string;
image_width: number;
image_height: number;
project_url: string;
};

export type CaseStudyLayoutProps = {
Expand All @@ -42,6 +44,7 @@ export function CaseStudyLayout({ children }: CaseStudyLayoutProps) {
const { title, frontMatter } = useConfig<CaseStudyFrontmatter>();

const { prev, next } = getPrevAndNextPagesByTitle(title, '/pro/case-studies');

return (
<>
<div className="max-w-3xl mx-auto px-6">
Expand All @@ -68,7 +71,12 @@ export function CaseStudyLayout({ children }: CaseStudyLayoutProps) {
</Container>

<div className="max-w-3xl mx-auto px-6">
<>{children}</>
{children}
<Button asChild>
<a href={frontMatter.project_url} target="_blank">
Visit Project Website
</a>
</Button>
</div>

<div className="mx-auto max-w-screen-xl">
Expand Down
200 changes: 152 additions & 48 deletions packages/xy-shared/layouts/showcase.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
'use client';

import { useCallback, useMemo, useState, ReactNode } from 'react';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { cn, ContentGrid, ContentGridItem } from '@xyflow/xy-ui';
import { ArrowRightCircleIcon } from '@heroicons/react/24/solid';
import { RocketLaunchIcon } from '@heroicons/react/24/outline';
import {
Button,
cn,
Container,
ContentGrid,
ContentGridItem,
Heading,
Link,
Text,
} from '@xyflow/xy-ui';
import { type MdxFile } from 'nextra';
import Image from 'next/image';

import { BaseLayout, ProjectPreview, Hero } from '../';
import { BaseLayout } from './base';
import { ProjectPreview } from '../widgets/project-preview';
import { Hero } from '../widgets/hero';
import { type CaseStudyFrontmatter } from './case-study';

export type CaseStudy = MdxFile<CaseStudyFrontmatter>;

export type ShowcaseLayoutProps = {
title: string;
subtitle: string;
showcases?: ShowcaseItem[];
caseStudies?: CaseStudy[];
children?: ReactNode;
};

Expand All @@ -20,35 +38,57 @@ export type ShowcaseItem = {
image: string;
url: string;
demoUrl?: string;
repoUrl?: string;
openSource?: boolean;
tags: { id: string; name: string }[];
};

function isCaseStudy(item: CaseStudy | ShowcaseItem): item is CaseStudy {
return item.hasOwnProperty('frontMatter');
}

export function ShowcaseLayout({
title,
subtitle,
showcases = [],
caseStudies = [],
children,
}: ShowcaseLayoutProps) {
const { all, selected, toggle } = useTags(showcases);
const visibleShowcases = useMemo(() => {
if (selected.size === 0) {
return showcases;
}
return showcases.filter(({ tags }) =>
Array.from(selected).every((tag) =>
tags.some(({ name }) => name === tag),
),

const visibleItems = useMemo(() => {
const visibleShowcases = showcases.filter(
({ tags }) =>
selected.size === 0 ||
Array.from(selected).every((tag) =>
tags.some(({ name }) => name === tag),
),
);

let currentCaseStudy = caseStudies[0];

return visibleShowcases.reduce(
(list, showcase, i) => {
list.push(showcase);
if (currentCaseStudy && (i + 1) % 6 === 0) {
list.push(currentCaseStudy);
currentCaseStudy = caseStudies[(i + 1) / 6];
}
return list;
},
[] as (ShowcaseItem | CaseStudy)[],
);
}, [selected, showcases]);
}, [selected, showcases, caseStudies]);

return (
<BaseLayout>
<Hero
kicker="Showcase"
kickerIcon={MagnifyingGlassIcon}
kickerIcon={RocketLaunchIcon}
title={title}
subtitle={subtitle}
align="center"
backgroundVariant="gradient"
/>

<div className="flex justify-center items-center flex-wrap gap-x-2 gap-y-4 max-w-4xl mx-auto">
Expand All @@ -62,46 +102,58 @@ export function ShowcaseLayout({
))}
</div>

<ContentGrid className="mt-8">
{visibleShowcases.map((showcase) => (
<ContentGridItem key={showcase.id}>
<ProjectPreview
image={`/img/showcase/${showcase.image}`}
title={showcase.title}
subtitle={
<>
<span className="flex gap-2">
{showcase.tags.map((tag) => (
<Tag
key={tag.id}
name={tag.name}
selected={selected.has(tag.name)}
onClick={toggle}
/>
))}
</span>
</>
}
description={showcase.description}
route={showcase.url}
altRoute={
showcase.demoUrl
? { href: showcase.demoUrl, label: 'Demo' }
: undefined
}
linkLabel="Website"
<ContentGrid className="mt-8 md:grid-cols-2 lg:grid-cols-3 border-none gap-4 lg:gap-8">
{visibleItems.map((item) =>
isCaseStudy(item) ? (
<CaseStudyPreview
key={item.name}
data={item.frontMatter as CaseStudyFrontmatter}
route={item.route}
/>
</ContentGridItem>
))}
) : (
<ContentGridItem
key={item.id}
className="border-none py-6 lg:py-8 lg:px-0 hover:bg-white relative"
>
<ProjectPreview
image={item.image}
title={item.title}
className="relative h-full flex-col flex"
imageWrapperClassName="w-full"
subtitle={
<>
<span className="flex gap-2">
{item.tags.map((tag) => (
<Tag
key={tag.id}
name={tag.name}
selected={selected.has(tag.name)}
onClick={toggle}
/>
))}
</span>
</>
}
description={item.description}
route={item.url}
altRoute={
item.demoUrl
? { href: item.demoUrl, label: 'Demo' }
: item.repoUrl
? { href: item.repoUrl, label: 'Repo' }
: undefined
}
linkLabel="Website"
/>
</ContentGridItem>
),
)}

<ContentGridItem
route="https://github.com/xyflow/web/issues/new?labels=content&template=submit-showcase.yaml"
className={showcases.length % 2 === 0 ? 'lg:col-span-2' : ''}
>
<ContentGridItem route="https://wbkd.notion.site/17bf4645224281e4bf61ce34fa671059">
<ProjectPreview
title="Your project here?"
description="Have you built something exciting you want to show off? We want to feature it here!"
linkLabel="Open an issue on GitHub"
linkLabel="Submit your project"
/>
</ContentGridItem>
</ContentGrid>
Expand Down Expand Up @@ -159,3 +211,55 @@ function useTags(showcases: ShowcaseItem[]) {

return { all, selected, toggle };
}

function CaseStudyPreview({
data,
route,
}: {
data: CaseStudyFrontmatter;
route: string;
}) {
return (
<Container
variant="dark"
className="col-span-full"
innerClassName="px-4 py-8 flex flex-wrap gap-12 relative w-full items-center shadow-none bg-none bg-gray-100/10 lg:px-12 lg:py-12"
>
<div className="max-md:w-full max-md:order-2 md:w-1/2">
<Text className="text-primary mb-4">{data.client}</Text>
<Heading size="sm" className="mb-4">
{data.title}
</Heading>
<Text className="mb-6">{data.description}</Text>
<div className="grid md:flex gap-4">
<Button
asChild
size="lg"
variant="secondary"
className="text-black hover:bg-gray-100 w-full md:w-auto"
>
<Link href={route}>Read Case Study</Link>
</Button>
<Button asChild variant="link" className="text-md font-bold">
<a
href={data.project_url}
target="_blank"
className="flexitems-center"
>
Project Website <ArrowRightCircleIcon className="ml-1 w-4 h-4" />
</a>
</Button>
</div>
</div>
<div className="max-md:w-full max-md:order-1 aspect-video md:w-1/2 relative flex-1 rounded-md overflow-hidden">
<Image
src={data.image}
alt={data.title}
fill
className="object-cover w-full h-full"
sizes="(max-width: 768px) 100vw, 500px"
/>
</div>
</Container>
);
}
4 changes: 3 additions & 1 deletion packages/xy-shared/lib/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ export function getPrevAndNextPagesByTitle<InternalRoute extends string>(
title: string,
route: InternalRoute,
) {
const pages = getMdxPagesUnderRoute(route);
const pages = getMdxPagesUnderRoute(route).filter(
(page) => page.name !== 'index',
);

const currentIndex = pages.findIndex(
(page) => page.frontMatter?.title === title,
Expand Down
4 changes: 4 additions & 0 deletions packages/xy-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"generate:component": "turbo gen react-component"
},
"devDependencies": {
"@types/node": "^22.10.7",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"dotenv": "^16.4.5",
"eslint": "^8.57.1",
"eslint-config-xyflow": "workspace:*",
"react": "^18.3.1",
Expand All @@ -23,6 +25,7 @@
"@docsearch/css": "^3.6.2",
"@docsearch/react": "^3.6.2",
"@heroicons/react": "^2.1.5",
"@notionhq/client": "^2.2.15",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.2",
Expand All @@ -36,6 +39,7 @@
"@react-three/fiber": "^8.17.10",
"@sindresorhus/slugify": "^2.2.1",
"@stackblitz/sdk": "^1.11.0",
"@vercel/blob": "^0.27.1",
"@xyflow/react": "^12.3.2",
"@xyflow/xy-ui": "workspace:*",
"clsx": "^2.1.1",
Expand Down
Loading

0 comments on commit 8950591

Please sign in to comment.