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.