Skip to content

Commit

Permalink
Add a tutorial on how to create slide show presentations with React F…
Browse files Browse the repository at this point in the history
…low. (#438)

* 💩

* ♻️ Refactor complete App example to match repo.

* 📝 Add comment encouraging folks to play around with the slides object to see changes.

* ♻️ Consistently use ReactFlow default export.

* ✨ Create a focus-on-click example app.

* 📝 Write more words.

* 🚧 Keep hammering away.

* ♻️ Refactor app to match standalone repo.

* minor fixes to copy and code snippets

* 🐛 Fixed a bug where slide controls were partially hidden by sandbox controls.

* 📝 Callout the need for parent element to have width/height.

* 📝 Add 'minZoom' prop to code examples, explain why.

* 📝 Consistently name main component 'App' in code snippets.

* 📝 Add missing 'initialSlide' definition in code snippet.

* 📝 Tweak higlighted lines in codesnippet.

* 📝 Make clearer that the 'FitViewOptions' snippet is from the api reference.

* 🚚 Move url to 'slide-shows-with-react-flow'.

* 🚚 Move url to 'slide-shows-with-react-flow'.

* 🔧 Fix typo in config.

* add tutorial image for socials, minor copy tweaks

* minor css tweaks for demo at top of tutorial

* 🎨 Align styles in all slide show examples.

* ♻️ Upgrade examples to reactflow v12.

* ♻️ Update tutorial to reactflow v12.

* ♻️ Update examples to reactflow v12.

* 📝 Update tutorial examples to reactflow v12.

* ✨ Add whats-new entry.

---------

Co-authored-by: johnrobbjr <[email protected]>
  • Loading branch information
hayleigh-dot-dev and johnrobbjr authored Jul 22, 2024
1 parent f25fa21 commit edb1e46
Show file tree
Hide file tree
Showing 32 changed files with 12,209 additions and 11,915 deletions.
21,946 changes: 10,037 additions & 11,909 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions sites/reactflow.dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
"react-player": "^2.12.0",
"react-remark": "^2.1.0",
"reactflow": "^11.11.4",
"styled-components": "^6.1.11",
"timeago-react": "^3.0.6",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { type Node, type NodeProps, useReactFlow } from '@xyflow/react';
import { Remark } from 'react-remark';
import { useCallback } from 'react';

export type SlideNode = Node<SlideData, 'slide'>;

export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};

export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;

// The padding constant is used when computing the presentation layout. It adds
// a bit of space between each slide
export const SLIDE_PADDING = 100;

const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;

export function Slide({ data }: NodeProps<SlideNode>) {
const { source, left, up, down, right } = data;
const { fitView } = useReactFlow();

const moveToNextSlide = useCallback(
(event: React.MouseEvent, id: string) => {
// Prevent the click event from propagating so `onNodeClick` is not
// triggered when clicking on the control buttons.
event.stopPropagation();
fitView({ nodes: [{ id }], duration: 100 });
},
[fitView],
);

return (
<article className="slide" style={style}>
<Remark>{source}</Remark>
<footer className="slide__controls nopan">
{left && <button onClick={(e) => moveToNextSlide(e, left)}></button>}
{up && <button onClick={(e) => moveToNextSlide(e, up)}></button>}
{down && <button onClick={(e) => moveToNextSlide(e, down)}></button>}
{right && <button onClick={(e) => moveToNextSlide(e, right)}></button>}
</footer>
</article>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
body {
margin: 0;
background-color: #f8f8f8;
font-family: sans-serif;
color: #111;
}

html,
body,
#root {
height: 100%;
}

.slide {
box-sizing: border-box;
box-shadow: 0rem 1rem 4rem 0.25rem rgba(0, 0, 0, 0.06);
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 6rem;
padding-top: 10rem;
padding-left: 8rem;
font-size: 4rem;
}

.slide h1 {
font-size: 8rem;
margin-bottom: 1rem;
}

.slide ul li {
margin-bottom: 1rem;
}

.slide__controls {
position: absolute;
bottom: 4rem;
right: 4rem;
display: flex;
justify-content: end;
gap: 1rem;
}

.slide__controls button {
font-size: 4rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 2px solid #000;
background-color: #fff;
color: #111;

&:hover {
background-color: #f8f8f8;
}

&:active {
background-color: #e0e0e0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { KeyboardEventHandler, useCallback, useState } from 'react';
import {
ReactFlow,
ReactFlowProvider,
useReactFlow,
NodeMouseHandler,
Background,
BackgroundVariant,
} from '@xyflow/react';

import '@xyflow/react/dist/style.css';
import './index.css';

import { Slide, SlideData } from './Slide';
import { slides, slidesToElements } from './slides';

const nodeTypes = {
slide: Slide,
};

const initialSlide = '01';
const { nodes, edges } = slidesToElements(initialSlide, slides);

function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();

const handleKeyPress = useCallback<KeyboardEventHandler>(
(event) => {
const slide = slides[currentSlide];

switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight': {
const direction = event.key.slice(5).toLowerCase() as keyof SlideData;
const target = slide[direction];

// Prevent the arrow keys from scrolling the page when React Flow is
// only part of a larger application.
event.preventDefault();

if (target) {
setCurrentSlide(target);
fitView({ nodes: [{ id: target }], duration: 100 });
}
}
}
},
[fitView, currentSlide],
);

const handleNodeClick = useCallback<NodeMouseHandler>(
(_, node) => {
if (node.id !== currentSlide) {
setCurrentSlide(node.id);
fitView({ nodes: [{ id: node.id }], duration: 100 });
}
},
[fitView, currentSlide],
);

return (
<ReactFlow
nodes={nodes}
nodeTypes={nodeTypes}
nodesDraggable={false}
edges={edges}
fitView
fitViewOptions={{ nodes: [{ id: initialSlide }], duration: 100 }}
minZoom={0.1}
onKeyDown={handleKeyPress}
onNodeClick={handleNodeClick}
>
<Background color="#f2f2f2" variant={BackgroundVariant.Lines} />
</ReactFlow>
);
}

export default () => (
<ReactFlowProvider>
<App />
</ReactFlowProvider>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Edge, Node } from '@xyflow/react';
import { SLIDE_WIDTH, SLIDE_HEIGHT, SLIDE_PADDING, SlideData } from './Slide';

const slide01 = {
id: '01',
data: {
right: '02',
source: `
# Slide 1
- This is the first slide
- It has a right arrow to go to the next slide
`,
},
};

const slide02 = {
id: '02',
data: {
left: '01',
up: '03',
right: '04',
source: `
# Slide 2
- This is the second slide
- It has a left arrow to go back to the first slide
- It has an up arrow to go to the third slide
- It has a right arrow to go to the fourth slide
`,
},
};

const slide03 = {
id: '03',
data: {
down: '02',
source: `
# Slide 3
- This is the third slide
- It has a down arrow to go back to the second slide
`,
},
};

const slide04 = {
id: '04',
data: {
left: '02',
source: `
# Slide 4
- This is the fourth slide
- It has a left arrow to go back to the second slide
`,
},
};

export const slides = Object.fromEntries(
[slide01, slide02, slide03, slide04].map(({ id, data }) => [id, data]),
) as Record<string, SlideData>;

export const slidesToElements = (
initial: string,
slides: Record<string, SlideData>,
) => {
const stack = [{ id: initial, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes: Node<SlideData>[] = [];
const edges: Edge[] = [];

while (stack.length) {
const { id, position } = stack.pop()!;
const data = slides[id];
const node = { id, type: 'slide', position, data };

if (data.left && !visited.has(data.left)) {
const nextPosition = {
x: position.x - (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};

stack.push({ id: data.left, position: nextPosition });
edges.push({
id: `${id}->${data.left}`,
source: id,
target: data.left,
});
}

if (data.up && !visited.has(data.up)) {
const nextPosition = {
x: position.x,
y: position.y - (SLIDE_HEIGHT + SLIDE_PADDING),
};

stack.push({ id: data.up, position: nextPosition });
edges.push({ id: `${id}->${data.up}`, source: id, target: data.up });
}

if (data.down && !visited.has(data.down)) {
const nextPosition = {
x: position.x,
y: position.y + (SLIDE_HEIGHT + SLIDE_PADDING),
};

stack.push({ id: data.down, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.down,
});
}

if (data.right && !visited.has(data.right)) {
const nextPosition = {
x: position.x + (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};

stack.push({ id: data.right, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.right,
});
}

nodes.push(node);
visited.add(id);
}

return { nodes, edges };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type Node, type NodeProps } from '@xyflow/react';
import { Remark } from 'react-remark';

export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;

export type SlideNode = Node<SlideData, 'slide'>;

export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};

const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;

export function Slide({ data }: NodeProps<SlideNode>) {
return (
<article className="slide" style={style}>
<Remark>{data.source}</Remark>
</article>
);
}
Loading

0 comments on commit edb1e46

Please sign in to comment.