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

Update showcase page #647

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
172 changes: 127 additions & 45 deletions packages/xy-shared/layouts/showcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@

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

import { BaseLayout, ProjectPreview, Hero } from '../';

export type CaseStudyFrontMatter = {
title: string;
client: string;
};

export type CaseStudy = MdxFile<CaseStudyFrontMatter>;

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

Expand All @@ -23,23 +41,42 @@ export type ShowcaseItem = {
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),
),
);
}, [selected, showcases]);

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, caseStudies]);

return (
<BaseLayout>
Expand All @@ -62,42 +99,46 @@ 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"
/>
</ContentGridItem>
))}
<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} />
) : (
<ContentGridItem
key={item.id}
className="border-none py-6 lg:py-8 lg:px-0 hover:bg-white group"
>
<ProjectPreview
image={`/img/showcase/${item.image}`}
title={item.title}
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' }
: 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://github.com/xyflow/web/issues/new?labels=content&template=submit-showcase.yaml">
<ProjectPreview
title="Your project here?"
description="Have you built something exciting you want to show off? We want to feature it here!"
Expand Down Expand Up @@ -159,3 +200,44 @@ function useTags(showcases: ShowcaseItem[]) {

return { all, selected, toggle };
}

function CaseStudyPreview({ data }: { data?: CaseStudyFrontMatter }) {
return (
<Container
variant="dark"
className="max-lg:rounded-none col-span-full"
innerClassName="px-4 py-8 flex flex-wrap gap-4 relative w-full items-center shadow-none bg-none bg-gray-100/10 lg:px-20 lg:py-20"
>
<div className="max-md:w-full md:flex-1">
<Text className="text-gray-400 mb-4">{data?.client}</Text>
<Heading size="md">{data?.title}</Heading>
</div>
<div className="max-md:w-full md:flex-1">
<Text className="mb-8 text-gray-300">
Get all 10 pro examples with just one month of a Pro subscription from
129€
</Text>
<div className="flex flex-wrap gap-2 mt-4">
<Button
asChild
size="lg"
variant="secondary"
className="text-black hover:bg-gray-100 w-full md:w-auto"
>
<Link href={`${process.env.NEXT_PUBLIC_PRO_PLATFORM_URL}/signup`}>
Try it out
</Link>
</Button>
<Button
asChild
size="lg"
variant="black"
className="bg-white/10 hover:bg-white/20 w-full md:w-auto"
>
<Link href="/pro/pricing">See subscription plans</Link>
</Button>
</div>
</div>
</Container>
);
}
21 changes: 15 additions & 6 deletions sites/reactflow.dev/src/layouts/showcase.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { ShowcaseLayout, SubscribeSection } from 'xy-shared';
import showcases from '../../public/data/showcases.json';
import {
CaseStudy,
getMdxPagesUnderRoute,
ShowcaseLayout,
SubscribeSection,
} from 'xy-shared';

// @todo this should be moved into getStaticProps
// if we have the data, it should be filtering out the react showcases from the list
const visibleShowcases = showcases;
import { useData } from 'nextra/hooks';

export default function Showcase() {
const { showcases } = useData();

const caseStudies = getMdxPagesUnderRoute('/pro/case-studies').filter(
(page) => page.name !== 'index',
);

return (
<ShowcaseLayout
title="See what you can build with React Flow"
subtitle="We've seen React Flow used to create data processing tools, chatbot builders, machine learning, musical synthesizers, and more. Explore some of our favorite projects from around the internet."
showcases={visibleShowcases}
showcases={showcases}
caseStudies={caseStudies as CaseStudy[]}
>
<SubscribeSection />
</ShowcaseLayout>
Expand Down
4 changes: 4 additions & 0 deletions sites/reactflow.dev/src/pages/showcase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ title: Showcase
description: Projects and examples using React Flow
---

import getStaticPropsShowcases from '@/utils/get-static-props/showcases';

export const getStaticProps = getStaticPropsShowcases;

import ShowcasePage from '@/layouts/showcase';

<ShowcasePage />
89 changes: 89 additions & 0 deletions sites/reactflow.dev/src/utils/get-static-props/showcases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require('dotenv').config({ path: '.env.local' });
const { Client } = require('@notionhq/client');
const path = require('path');
const fs = require('fs');
const https = require('https');

const SHOWCASES_DATABASE_ID = '17bf4645224280ff9710d495e21ed13d';
const notion = new Client({ auth: process.env.NOTION_API_SECRET });
const OUTPUT_IMAGE_PATH = path.resolve(__dirname, '../public/img/showcase');

const downloadImage = (source, target) => {
return new Promise((resolve) => {
https.get(source, (res) => {
res.pipe(fs.createWriteStream(target));
resolve(true);
});
});
};

// https://www.notion.so/wbkd/17bf4645224280ff9710d495e21ed13d?v=17bf4645224281c6a574000c4f316554&pvs=4

export default async function getStaticProps() {
const { results } = await notion.databases.query({
database_id: SHOWCASES_DATABASE_ID,
filter: {
and: [
{
property: 'Status',
select: {
equals: 'published',
},
},
{
property: 'Library',
select: {
equals: 'React Flow',
},
},
],
},
sorts: [
{
property: 'Featured',
direction: 'descending',
},
{
property: 'title',
direction: 'ascending',
},
],
});

const showcases = await Promise.all(
results.map(async (result) => {
const id = result.id;
const title = result.properties.Name.title[0].plain_text;
const projectUrl = result.properties['Project Website'].url;
const demoUrl = result.properties['Demo URL'].url;
const tags = result.properties.Tags.multi_select;
const featured = result.properties.Featured.checkbox;
const description = result.properties.Description.rich_text[0].plain_text;
const imageSrc = result.properties.Image.files[0].file.url;
const imageFileName = `${id}.png`;
const imageFilePath = path.resolve(OUTPUT_IMAGE_PATH, imageFileName);

await downloadImage(imageSrc, imageFilePath);

return {
id,
title,
url: projectUrl,
demoUrl,
description,
image: imageFileName,
tags,
featured,
};
}),
);

return {
props: {
ssg: {
showcases: showcases,
},
},
revalidate: 60 * 60 * 24,
};
}
2 changes: 1 addition & 1 deletion sites/svelteflow.dev/src/layouts/showcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import showcases from '../../public/data/showcases.json';
export default function Showcase() {
return (
<ShowcaseLayout
title="Craft incredible experiences with Svelte Flow."
title="See what you can build with Svelte Flow"
subtitle="We've seen people create data processing tools, chatbot builders, ML pipelines, and more with React Flow. Now we're bringing the same power to Svelte. In this showcase we collect our favorite projects."
showcases={showcases}
/>
Expand Down
Loading