Markdown Previewer
Intro
This is the second project in the freeCodeCamp Front End Development Libraries Certification. The objective is to build a website that converts markdown text entered by the user to HTML preview. The suggested method is to use a front end library like React. Read more about the project at Build a Markdown Previewer
Check out the end product at https://markdown-preview.projects.yasakdogra.com
Planning
I am going to use React, Redux and Tailwind CSS for building the user interface. As mentioned on the project specifications’ page, we can use Marked for parsing markdown to HTML.
NOTE: We don’t need have to use Redux for state management. We will only save the markdown entered by the user in the state, which could be done easily using React’s useState
method.
For basic project setup, check out React with Redux, Tailwind and More
We will need to add marked
package from NPM repository
$ bun add marked
Since we are using React, we are going to think in components and create some custom components to implement the UI.
Inside the App
component, we will create a SplitView
component with two View
components, one for markdown and other for preview. On wider displays, we will have the two views side by side and on thinner devices we will have them stack vertically. Tailwind CSS uses “mobile-first breakpoint system”, so we can add classes for mobile first and then add breakpoints for wider displays.
The view for markdown will have Editor
component where user will enter the markdown.
The view for preview will have Viewer
component where we will add the HTML generated by the markdown parser.
NOTE: We can also combine the code in the View
component with Editor
and Viewer
components and skip the View
component
We will only store the text entered by the user in the application state. The HTML can be generated on demand whenever the state changes.
State
Create a file src/state/markdownSlice.ts and import from redux toolkit
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
Create an interface for markdown state and create an initial state
interface MarkdownState {
text: string;
}
const initialState: MarkdownState = {
text: '# Markdown Preview\n'+
'\n'+
'## What is this?\n'+
'This is a markdown previewer. Write markdown in the **Editor** tab and watch the output in the **Preview** tab.\n'+
'Uses [mardown](https://marked.js.org/) compiler.\n'+
'\n'+
'## Examples\n'+
'List:\n'+
' - First item\n'+
' - Second item\n'+
'\n'+
'Inline code: `let x = 10;`\n'+
'\n'+
'Code block:\n'+
'```js\n'+
'let x = 10;\n'+
'let y = x + 10;\n'+
'```\n'+
'\n'+
'Blockquote: \n'+
'> Blockquote example\n'+
'\n'+
'Image:\n'+
'![Sample image](/vite.svg)\n'+
'\n'+
'Link:\n'+
'[mardown](https://marked.js.org/)'
}
Use createState to generate action creators and action types
export const markdownSlice = createSlice({
name: 'markdown',
initialState,
reducers: {
updateMarkdown: (state, action: PayloadAction<string>) => {
state.text = action.payload;
}
}
})
Export the action and reducer we generated
export const { updateMarkdown } = markdownSlice.actions
export default markdownSlice.reducer
In the src/state/store.ts, add the reducer we exported
import { configureStore } from "@reduxjs/toolkit"
import markdownReducer from "./markdownSlice"
export const store = configureStore({
reducer: {
markdown: markdownReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
This is all we need to save the markdown in the state and access it later for parsing.
Components
We will start with the SplitView
component. This is going to be a functional component. It will take a list of ReactNode
children as props. It will render each child in a separate div
inside a parent div
. The parent div will handle the responsive layout with Tailwind CSS classes. We will add grid
class to make the parent div a grid, grid-cols-1
to have one column on smaller screens and md:grid-cols-2
to have two columns on wider screens.
Create src/component/SplitView.tsx
export default function SplitView({ children } : { children: React.ReactNode[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2">
{children.map((child, i) => (
<div key={i}>
{child}
</div>
))}
</div>
)
}
Now we will create the View
component. It will take a title and a ReactNode
child as props. It will display the title bar on top and render the child under it. We can apply some styling to the title bar using Tailwind classes. Add background with bg-gray-500
, make it sticky
top so the user can scroll contents with the bar still showing on top, center text with text-center
and add some more styling like padding, font-size, etc. Note that we have to use the name children
in our props even though it will be a single node and not a list.
Create src/component/View.tsx
type ViewProps = {
title: string;
children: React.ReactNode;
}
export default function View({ title, children }: ViewProps) {
return (
<div className="p-4 h-full">
<div className="bg-gray-500 py-2 px-4 sticky object-top text-center text-xl text-white font-bold">
{title}
</div>
{children}
</div>
)
}
The next two components will use state provided by Redux. Let’s start with the Editor
component. This component does not need any props. This component will essentially be a textarea
. It will need to read the state to set its own contents and dispatch action to modify the state. In the body of the component we use useSelector to get state.markdown.text
from Redux state store and use it to set the value
of the textarea
. Then we get the dispatch function with useDispatch
and use it to dispatch updateMarkdown
action when the value of the textarea
changes.
Create src/components/Editor.tsx
import { useDispatch, useSelector } from "react-redux"
import { AppDispatch, RootState } from "../state/store"
import { updateMarkdown } from "../state/markdownSlice"
export default function Editor() {
const text = useSelector((state: RootState) => state.markdown.text)
const dispatch = useDispatch<AppDispatch>()
return (
<textarea
id="editor"
className="bg-white w-full align-top p-4 h-[calc(100vh-75px)] max-h-[calc(100vh-75px)] outline-none"
value={text}
onChange={(e) => dispatch(updateMarkdown(e.target.value))}
></textarea>
)
}
We will use a similar method for Viewer
component. This one does not need to dispatch actions, so we only need to read the state and create HTML from it. We will need to import and use marked
to convert the markdown to HTML. We can set the contents of our div
with the generated HTML with dangerouslySetInnerHTML
.
Create src/components/Viewer.tsx
import { useSelector } from "react-redux"
import { RootState } from "../state/store"
import { marked } from "marked"
export default function Viewer() {
const markdown = useSelector((state: RootState) => state.markdown.text)
return (
<div
id="preview"
className="bg-white overflow-scroll w-full align-top p-4 max-w-none h-[calc(100vh-75px)] max-h-[calc(100vh-75px)] prose"
dangerouslySetInnerHTML={{__html: marked(markdown, {breaks: true})}}
>
</div>
)
}
Now all that’s left is to use the components we created above inside the App
component in our application. We will add a SplitView
and add two View
children with different titles. We add Editor
as a child to the view with title ‘Editor’ and Preview
as child component to the view with title ‘Preview’.
In the src/App.tsx
function App() {
return (
<SplitView>
<View title='Editor'>
<Editor />
</View>
<View title='Preview'>
<Viewer />
</View>
</SplitView>
)
}
We can start a development server with bun run dev
, or package the application with bun run build
and test with bun run preview
. The build will generate files under dist folder. These are just HTML, JS and CSS files which can be hosted as a static website.
Thank you for reading. You can also check out my other projects for this series below.