Skip to content

Instantly share code, notes, and snippets.

@elliottsj
Last active November 17, 2021 21:51
Show Gist options
  • Save elliottsj/91b904e12cc11d4a8148d882fe3017a2 to your computer and use it in GitHub Desktop.
Save elliottsj/91b904e12cc11d4a8148d882fe3017a2 to your computer and use it in GitHub Desktop.
Basic stories in Next.js 11 (put `[...slug].tsx` and `index.tsx` in `pages/`, stories in `stories/`)
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import React from 'react';
import { css, Global } from '@emotion/core';
interface StoryLocation {
modulePath: string;
storyName: string;
}
/**
* Given a slug parameter, determine the module path and story name.
*
* @example
* '/Avatar.stories/playground'
* -> ['Avatar.stories', 'playground']
* -> { modulePath: 'Avatar.stories', storyName: 'playground' }
*
* @example
* '/Dropdowns/AsyncMultiDropdown.stories/asyncOptions'
* -> ['Dropdowns', 'AsyncMultiDropdown.stories', 'asyncOptions']
* -> { modulePath: 'Dropdowns/AsyncMultiDropdown.stories', storyName: 'asyncOptions' }
*/
const getStoryLocation = (slug?: string | string[]): StoryLocation | null => {
if (!Array.isArray(slug) || slug.length < 2) {
return null;
}
const modulePath = slug.slice(0, -1).join('/');
const storyName = slug[slug.length - 1];
return { modulePath, storyName };
};
/**
* Render a single story.
*/
const StoryPage: React.FC = () => {
const router = useRouter();
const storyLocation = getStoryLocation(router.query.slug);
if (!storyLocation) {
return <div>Story not found.</div>;
}
const Story = dynamic(async () => {
const storyModule = await import(`../stories/${storyLocation.modulePath}.tsx`);
const stories = { ...storyModule };
delete stories.default;
return stories[storyLocation.storyName];
});
return (
<>
<Global
styles={css`
@font-face {
font-family: 'Sofia Pro';
src: url('/static/fonts/SofiaPro-Regular.woff2') format('woff2'),
url('/static/fonts/SofiaPro-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
}
`}
/>
<Story />
</>
);
};
export default StoryPage;
import React from 'react';
import { ActionList, ActionItemButton } from '../components';
import { css } from '@emotion/core';
import { Example, Title } from './Utilities';
export default {
title: 'ActionList',
};
export const Buttons = () => (
<Example>
<Title>Buttons</Title>
<div
css={css`
padding: 1rem;
`}
>
<ActionList>
<ActionItemButton>One</ActionItemButton>
<ActionItemButton>Two</ActionItemButton>
<ActionItemButton>Three</ActionItemButton>
</ActionList>
</div>
</Example>
);
import { GetServerSideProps } from 'next';
import Link from 'next/link';
import path from 'path';
import React from 'react';
type RequireContextType = ReturnType<typeof require.context>;
interface StoryModule {
title: string;
path: string;
stories: Story[];
}
interface Story {
name: string;
href: string;
}
interface StoriesPageProps {
stories: StoryModule[];
}
/**
* An index of every story in the project.
*/
const StoriesPage: React.FC<StoriesPageProps> = (props) => {
return (
<ul className="stories" data-stories={JSON.stringify(props.stories)}>
{props.stories.map((storyModule) => (
<li key={storyModule.path}>
<span>{storyModule.title}</span>
<ul>
{storyModule.stories.map((story) => (
<li key={story.href}>
<Link href={story.href}>{story.name}</Link>
</li>
))}
</ul>
</li>
))}
</ul>
);
};
export default StoriesPage;
/**
* Given a story module name from a webpack context, compute the path to the story module.
*
* @example
* > getPath('Button.stories.tsx')
* 'Button.stories'
*
* @example
* > getPath('DropdownButton/DropdownButton.stories.tsx')
* 'DropdownButton/DropdownButton.stories'
*/
const getPath = (moduleName: string) => moduleName.match(/^(?<slug>.+)\.tsx$/)?.groups?.slug;
const getStoryModules = async (storyModulesReq: RequireContextType): Promise<StoryModule[]> => {
const storyModules: StoryModule[] = storyModulesReq
.keys()
.map((moduleName) => {
const modulePath = getPath(moduleName);
if (!modulePath) throw new Error(`Could not compute path for "${moduleName}"`);
return { moduleName, path: modulePath };
})
.map(({ moduleName, path }) => {
const mod = storyModulesReq(moduleName);
const { default: meta, ...stories } = mod;
if (!meta) {
throw new Error(
`Story module "${moduleName}" is missing a default meta export. ` +
`Add \`export default { title: 'Your title' };\``,
);
}
if (!meta.title) {
throw new Error(
`Story module "${moduleName}" is missing a title from its default meta export. ` +
`Add \`export default { title: 'Your title' };\``,
);
}
return {
title: meta.title,
path,
stories: Object.keys(stories).map((storyName) => ({
name: storyName,
href: `/${path}/${storyName}`,
})),
};
});
return storyModules;
};
export const getServerSideProps: GetServerSideProps = async () => ({
props: {
stories: await getStoryModules(require.context('../stories', true, /\.stories\.tsx$/)),
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment