Skip to content

Commit

Permalink
feat(lib, src): add Article component and integrate with MDX, refacto…
Browse files Browse the repository at this point in the history
…r related components
  • Loading branch information
websiddu committed Dec 18, 2024
1 parent 847f4a1 commit 67a75f5
Show file tree
Hide file tree
Showing 13 changed files with 1,582 additions and 35 deletions.
93 changes: 93 additions & 0 deletions lib/Article/Article.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createElement, useEffect, useState } from 'react';
import { Note, Tip, Steps, Step } from '@stubbycms/ui';

Check failure on line 2 in lib/Article/Article.tsx

View workflow job for this annotation

GitHub Actions / Release

Cannot find module '@stubbycms/ui' or its corresponding type declarations.
import { highlight } from 'sugar-high';
import { removeFrontMatter, slugify } from '../utils';
import remarkGfm from 'remark-gfm';
import { MDXRemote } from 'next-mdx-remote/rsc';

const mdxOptions = {
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [],
},
};

const CustomLink = (props: { href: string; children: React.ReactNode }) => {
let href = props.href;

if (href.startsWith('#')) {
return <a {...props} />;
}

return (
<a
target="_blank"
rel="noopener noreferrer"
{...props}
/>
);
};

const Code = ({ children, ...props }: { children: string }) => {
let codeHTML = highlight(children);
return (
<code
dangerouslySetInnerHTML={{ __html: codeHTML }}
{...props}
/>
);
};

function createHeading(level: number) {
const Heading = ({ children }: { children: string }) => {
let slug = slugify(children);
return createElement(
`h${level}`,
{ id: slug },
[
createElement('a', {
href: `#${slug}`,
key: `link-${slug}`,
className: 'anchor',
}),
],
children,
);
};

Heading.displayName = `Heading${level}`;

return Heading;
}

export const ArticleComponents: Record<string, React.ComponentType<any>> = {
h1: createHeading(1),
h2: createHeading(2),
h3: createHeading(3),
h4: createHeading(4),
h5: createHeading(5),
h6: createHeading(6),
a: CustomLink,
code: Code,
Note,
Tip,
Steps,
Step,
};

export function Article(props: {
components?: Record<string, React.ComponentType>;
source: string;
}) {
const [mdxContent, setMdxContent] = useState<React.ReactNode>(null);

useEffect(() => {
MDXRemote({
source: removeFrontMatter(props.source as string),
components: { ...ArticleComponents, ...(props.components || {}) },
options: mdxOptions,
}).then(setMdxContent);
}, [props.source, props.components]);

return <article className="prose">{mdxContent}</article>;
}
11 changes: 11 additions & 0 deletions lib/Code/Code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { highlight } from 'sugar-high';

export const Code = ({ children, ...props }: { children: string }) => {
let codeHTML = highlight(children);
return (
<code
dangerouslySetInnerHTML={{ __html: codeHTML }}
{...props}
/>
);
};
4 changes: 4 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export * from './Callout/Callout';
export * from './Accordion/Accordion';
export * from './Steps/Steps';
export * from './Tabs/Tabs';
export * from './Article/Article';
export * from './Code/Code';

export * from './utils';
23 changes: 23 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import _slugify from 'slugify';

/**
* Removes the front matter from a given content string.
*
* The front matter is expected to be enclosed within triple dashes (`---`).
* If front matter is found, it is removed from the content string.
* If no front matter is found, the original content string is returned.
*
* @param content - The content string from which to remove the front matter.
* @returns The content string without the front matter.
*/
export const removeFrontMatter = (content: string) => {
let frontMatter = content.match(/---([\s\S]*?)---/);
if (frontMatter) {
return content.replace(frontMatter[0], '');
}
return content;
};

export const slugify = (text: string) => {
return _slugify(text, { lower: true, strict: true });
};
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.2",
Expand Down Expand Up @@ -72,7 +73,11 @@
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.453.0",
"react-router": "^7.0.2"
"next-mdx-remote": "^5.0.0",
"react-router": "^7.0.2",
"remark-gfm": "^4.0.0",
"slugify": "^1.6.6",
"sugar-high": "^0.7.5"
},
"publishConfig": {
"access": "public"
Expand Down
28 changes: 1 addition & 27 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
import { NavLink } from 'react-router';
import { components } from './components/SideNav';

function App() {
const components = [
{
name: 'Tabs',
description:
'A tab system that allows users to switch between different sections of content.',
route: '/components/tabs',
},
{
name: 'Callouts',
description:
'A callout is a small piece of text that is used to "call out" a feature or highlight a specific piece of information.',
route: '/components/callouts',
},
{
name: 'Accordion',
description:
'An accordion is a vertically stacked list of items that utilizes show/hide functionality.',
route: '/components/accordion',
},
{
name: 'Steps',
description:
'A step component is used to guide users through a process or workflow in a linear fashion.',
route: '/components/steps',
},
];

return (
<div className="container mx-auto max-w-6xl">
<h1 className="text-xl font-bold mb-4 mt-20">Components</h1>
Expand Down
61 changes: 61 additions & 0 deletions src/components/SideNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NavLink } from 'react-router';

export const components = [
{
name: 'Tabs',
description: 'A tab system that allows users to switch between different sections of content.',
route: '/components/tabs',
},
{
name: 'Callouts',
description:
'A callout is a small piece of text that is used to "call out" a feature or highlight a specific piece of information.',
route: '/components/callouts',
},
{
name: 'Accordion',
description:
'An accordion is a vertically stacked list of items that utilizes show/hide functionality.',
route: '/components/accordion',
},
{
name: 'Steps',
description:
'A step component is used to guide users through a process or workflow in a linear fashion.',
route: '/components/steps',
},
{
name: 'Article',
description: 'A component that renders markdown content with custom components.',
route: '/components/article',
},
].sort((a, b) => a.name.localeCompare(b.name)) as {
name: string;
description: string;
route: string;
}[];

export const SideNav = () => {
return (
<div className="min-w-[220px]">
<div className="sticky top-10">
<h1 className="text-xs font-bold mb-4 uppercase tracking-widest">Components</h1>
<div className="flex flex-col gap-2">
{components.map((component) => (
<NavLink
to={component.route}
key={component.name}
className={({ isActive }) => {
const classNames =
'text-gray-700 hover:text-gray-900 px-4 py-1.5 hover:bg-slate-100 -ml-4 rounded-full';
return isActive ? `${classNames} bg-slate-100 font-medium` : classNames;
}}
>
{component.name}
</NavLink>
))}
</div>
</div>
</div>
);
};
9 changes: 6 additions & 3 deletions src/layouts/ComponentsLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Outlet } from 'react-router';
import { SideNav } from '../components/SideNav';

export function ComponentsLayout() {
return (
<div className="container mx-auto max-w-6xl">
<div className="h-20"></div>
<Outlet />
<div className="container mx-auto max-w-6xl flex pt-10 gap-10">
<SideNav />
<div className="flex-1">
<Outlet />
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { CalloutSamples } from './pages/CalloutSamples.tsx';
import { TabSamples } from './pages/TabSamples.tsx';
import { AccordionSamples } from './pages/AccordionSamples.tsx';
import { StepSamples } from './pages/StepSamples.tsx';
import { ArticleSamples } from './pages/ArticleSamples.tsx';

const components: { [key: string]: React.ReactElement } = {
accordion: <AccordionSamples />,
tabs: <TabSamples />,
callouts: <CalloutSamples />,
steps: <StepSamples />,
article: <ArticleSamples />,
};

createRoot(document.getElementById('root')!).render(
Expand All @@ -30,6 +32,7 @@ createRoot(document.getElementById('root')!).render(
{Object.keys(components).map((key) => {
return (
<Route
key={key}
path={key}
element={components[key]}
/>
Expand Down
12 changes: 12 additions & 0 deletions src/pages/ArticleSamples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Article } from '../../lib';
// @ts-ignore
import article from '../samples/article.mdx?raw';

export const ArticleSamples = () => {
return (
<>
<h1 className="text-4xl font-bold mb-6">Privacy-first programming</h1>
<Article source={article}></Article>
</>
);
};
Loading

0 comments on commit 67a75f5

Please sign in to comment.