From 5ed031242f284571217f9fd54236aea7be096542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 17 Jan 2025 21:55:47 +0100 Subject: [PATCH 1/3] feat(showcases): add case studies to showcase page --- packages/xy-shared/layouts/showcase.tsx | 171 ++++++++++++++----- sites/reactflow.dev/src/layouts/showcase.tsx | 11 +- 2 files changed, 136 insertions(+), 46 deletions(-) diff --git a/packages/xy-shared/layouts/showcase.tsx b/packages/xy-shared/layouts/showcase.tsx index 2aa56c823..1cd5f7957 100644 --- a/packages/xy-shared/layouts/showcase.tsx +++ b/packages/xy-shared/layouts/showcase.tsx @@ -2,7 +2,17 @@ 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 '../'; @@ -10,6 +20,7 @@ export type ShowcaseLayoutProps = { title: string; subtitle: string; showcases?: ShowcaseItem[]; + caseStudies?: MdxFile[]; children?: ReactNode; }; @@ -23,23 +34,44 @@ export type ShowcaseItem = { tags: { id: string; name: string }[]; }; +function isCaseStudy(item: MdxFile | ShowcaseItem): item is MdxFile { + 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 | MdxFile)[], ); - }, [selected, showcases]); + }, [selected, showcases, caseStudies]); + + console.log(caseStudies); return ( @@ -62,42 +94,46 @@ export function ShowcaseLayout({ ))} - - {visibleShowcases.map((showcase) => ( - - - - {showcase.tags.map((tag) => ( - - ))} - - - } - description={showcase.description} - route={showcase.url} - altRoute={ - showcase.demoUrl - ? { href: showcase.demoUrl, label: 'Demo' } - : undefined - } - linkLabel="Website" - /> - - ))} + + {visibleItems.map((item) => + isCaseStudy(item) ? ( + + ) : ( + + + + {item.tags.map((tag) => ( + + ))} + + + } + description={item.description} + route={item.url} + altRoute={ + item.demoUrl + ? { href: item.demoUrl, label: 'Demo' } + : undefined + } + linkLabel="Website" + /> + + ), + )} - + ['frontMatter']; +}) { + return ( + +
+ {data?.client} + {data?.title} +
+
+ + Get all 10 pro examples with just one month of a Pro subscription from + 129€ + +
+ + +
+
+
+ ); +} diff --git a/sites/reactflow.dev/src/layouts/showcase.tsx b/sites/reactflow.dev/src/layouts/showcase.tsx index 671b1c20f..f0a5ff832 100644 --- a/sites/reactflow.dev/src/layouts/showcase.tsx +++ b/sites/reactflow.dev/src/layouts/showcase.tsx @@ -1,4 +1,8 @@ -import { ShowcaseLayout, SubscribeSection } from 'xy-shared'; +import { + getMdxPagesUnderRoute, + ShowcaseLayout, + SubscribeSection, +} from 'xy-shared'; import showcases from '../../public/data/showcases.json'; // @todo this should be moved into getStaticProps @@ -6,11 +10,16 @@ import showcases from '../../public/data/showcases.json'; const visibleShowcases = showcases; export default function Showcase() { + const caseStudies = getMdxPagesUnderRoute('/pro/case-studies').filter( + (page) => page.name !== 'index', + ); + return ( From be84120427a1654ceedbf070131d8303beffb613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 17 Jan 2025 22:05:21 +0100 Subject: [PATCH 2/3] fix(build): ts errors in case study type --- packages/xy-shared/layouts/showcase.tsx | 21 ++++++++++---------- sites/reactflow.dev/src/layouts/showcase.tsx | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/xy-shared/layouts/showcase.tsx b/packages/xy-shared/layouts/showcase.tsx index 1cd5f7957..f109831b2 100644 --- a/packages/xy-shared/layouts/showcase.tsx +++ b/packages/xy-shared/layouts/showcase.tsx @@ -16,11 +16,18 @@ import { type MdxFile } from 'nextra'; import { BaseLayout, ProjectPreview, Hero } from '../'; +export type CaseStudyFrontMatter = { + title: string; + client: string; +}; + +export type CaseStudy = MdxFile; + export type ShowcaseLayoutProps = { title: string; subtitle: string; showcases?: ShowcaseItem[]; - caseStudies?: MdxFile[]; + caseStudies?: CaseStudy[]; children?: ReactNode; }; @@ -34,7 +41,7 @@ export type ShowcaseItem = { tags: { id: string; name: string }[]; }; -function isCaseStudy(item: MdxFile | ShowcaseItem): item is MdxFile { +function isCaseStudy(item: CaseStudy | ShowcaseItem): item is CaseStudy { return item.hasOwnProperty('frontMatter'); } @@ -67,12 +74,10 @@ export function ShowcaseLayout({ } return list; }, - [] as (ShowcaseItem | MdxFile)[], + [] as (ShowcaseItem | CaseStudy)[], ); }, [selected, showcases, caseStudies]); - console.log(caseStudies); - return ( ['frontMatter']; -}) { +function CaseStudyPreview({ data }: { data?: CaseStudyFrontMatter }) { return ( From ec29d65c69c83b1e23920573c9b572b140f52a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Sat, 18 Jan 2025 14:22:06 +0100 Subject: [PATCH 3/3] feat(showcases): retrieve showcases in getStaticProps --- sites/reactflow.dev/src/layouts/showcase.tsx | 9 +- sites/reactflow.dev/src/pages/showcase.mdx | 4 + .../src/utils/get-static-props/showcases.ts | 89 +++++++++++++++++++ sites/svelteflow.dev/src/layouts/showcase.tsx | 2 +- sites/svelteflow.dev/src/pages/_meta.ts | 33 +++---- 5 files changed, 108 insertions(+), 29 deletions(-) create mode 100644 sites/reactflow.dev/src/utils/get-static-props/showcases.ts diff --git a/sites/reactflow.dev/src/layouts/showcase.tsx b/sites/reactflow.dev/src/layouts/showcase.tsx index 13c6d5c95..92dde7b50 100644 --- a/sites/reactflow.dev/src/layouts/showcase.tsx +++ b/sites/reactflow.dev/src/layouts/showcase.tsx @@ -4,13 +4,12 @@ import { ShowcaseLayout, SubscribeSection, } from 'xy-shared'; -import showcases from '../../public/data/showcases.json'; -// @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', ); @@ -19,7 +18,7 @@ export default function Showcase() { diff --git a/sites/reactflow.dev/src/pages/showcase.mdx b/sites/reactflow.dev/src/pages/showcase.mdx index 92dc39f73..c82e3b02a 100644 --- a/sites/reactflow.dev/src/pages/showcase.mdx +++ b/sites/reactflow.dev/src/pages/showcase.mdx @@ -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'; diff --git a/sites/reactflow.dev/src/utils/get-static-props/showcases.ts b/sites/reactflow.dev/src/utils/get-static-props/showcases.ts new file mode 100644 index 000000000..d91d7506f --- /dev/null +++ b/sites/reactflow.dev/src/utils/get-static-props/showcases.ts @@ -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, + }; +} diff --git a/sites/svelteflow.dev/src/layouts/showcase.tsx b/sites/svelteflow.dev/src/layouts/showcase.tsx index 1753d5257..9362c8926 100644 --- a/sites/svelteflow.dev/src/layouts/showcase.tsx +++ b/sites/svelteflow.dev/src/layouts/showcase.tsx @@ -5,7 +5,7 @@ import showcases from '../../public/data/showcases.json'; export default function Showcase() { return ( diff --git a/sites/svelteflow.dev/src/pages/_meta.ts b/sites/svelteflow.dev/src/pages/_meta.ts index aa90512ea..4c2f765d4 100644 --- a/sites/svelteflow.dev/src/pages/_meta.ts +++ b/sites/svelteflow.dev/src/pages/_meta.ts @@ -8,30 +8,24 @@ export default { learn: 'Learn', 'api-reference': 'Reference', examples: 'Examples', - community: { - title: 'Community', + showcase: { + title: 'Showcase', + theme: { + layout: 'raw', + }, + }, + more: { + title: 'More', type: 'menu', items: { - showcase: { - title: 'Showcase', - href: '/showcase', - }, - 'whats-new': { - title: "What's new?", + changelog: { + title: 'Changelog', href: '/whats-new', }, blog: { title: 'Blog', href: 'https://xyflow.com/blog', }, - discord: { - title: 'Discord', - href: 'https://discord.gg/RVmnytFmGW', - }, - github: { - title: 'Github', - href: 'https://github.com/xyflow/xyflow', - }, contact: { title: 'Contact Us', href: 'https://xyflow.com/contact', @@ -46,13 +40,6 @@ export default { layout: 'raw', }, }, - showcase: { - title: 'Showcase', - display: 'hidden', - theme: { - layout: 'raw', - }, - }, 'support-us': { title: 'Support Us', display: 'hidden',