Screenshot of the Markee App interface
Category
Personal
Year

2021

Type
Web App
Overview

A distraction-free markdown editor that lives entirely in your browser. Write on one side, watch it render on the other, and every keystroke saves itself locally — no account, no server, nothing to lose.

Highlights
  • Built a two-pane editor — raw markdown on the left, a live rendered preview on the right — that collapses to a single column on small screens.

  • Persisted every file to the browser with localforage (IndexedDB): notes survive a refresh and a closed tab, with no backend behind them.

  • Designed an autosave state machine — editing → saving → saved — with a short debounce, so saving feels instant without thrashing storage on every keystroke.

  • Rendered markdown with marked and lit up fenced code blocks with highlight.js, loaded on demand to keep the editor light.

Stack
TypeScriptReactStyled Componentsmarkedhighlight.jslocalforageTesting Library

Markee is a markdown editor that runs entirely in your browser. I built it during the Brainn.Co bootcamp from a Figma the mentors handed us: write on the left, watch it render on the right, and never once think about saving. That last part turned out to be the whole personality of the app.

The premise: your notes, on your machine

A markdown editor is a small idea with a lot of hidden corners. I wanted one with no account, no sync, and no server to go down — you open the tab and your files are already there. That decision pushes every interesting problem onto the client: where do files live, when do they save, and how do you make "saved" feel trustworthy when there's no backend to confirm it?

One hook holds the whole app

Every file — its name, its content, which one is active, and its save status — lives in a single useFiles hook. The two surfaces, a sidebar list and the editor, are pure functions of that state; creating, switching, renaming, and deleting are all just transformations of one array:

const handleCreateNewFile = () => {
  const file = {
    id: uuidv4(),
    name: "Sem título",
    content: "",
    active: true,
    status: "saved",
  }
  setFiles((files) => [...files.map((f) => ({ ...f, active: false })), file])
}

Keeping it in one place meant I never had two components disagreeing about which file was open — a class of bug I'd created in earlier projects by scattering state around.

Making "saved" feel safe

Autosave is easy to get wrong in both directions: save on every keystroke and you hammer storage; save too rarely and you lose work. So saving is a tiny state machine — editing → saving → saved — driven by a debounce. The instant you type, the file is editing; once you stop, it settles through saving to saved:

useEffect(() => {
  const active = files.find((file) => file.active)
  if (active?.status !== "editing") return
 
  const timer = setTimeout(() => {
    setFiles((files) => setStatus("saving", files))
    setTimeout(() => setFiles((files) => setStatus("saved", files)), 300)
  }, 300)
 
  return () => clearTimeout(timer)
}, [files])

That status drives a small icon in the sidebar: a spinner while it saves, then a checkmark that draws itself on with an SVG stroke animation. It's a flicker of motion nobody asked for — and it's the exact moment the app earns your trust.

The persistence underneath is almost boring, which is the point. Whenever files change, localforage writes the array to IndexedDB; on load it reads it back, and if there's nothing there yet it creates your first file for you:

useEffect(() => {
  localforage.setItem("files", files)
}, [files])

The active file's id even rides along in the URL through history.replaceState, so a note feels like a place you can link to and come back to.

The preview

The right pane is your markdown, rendered. marked turns the text into HTML and highlight.js colors fenced code blocks — pulled in with a dynamic import() so the highlighter stays out of the bundle until there's actually code to highlight. On narrow screens the two panes stop sitting side by side and stack instead.

Good autosave is invisible. You should never have to wonder whether your work is safe — the interface should have already told you, quietly.

What I'd do differently

This was an early project, and it shows in ways both good and bad. useFiles grew into one hook that does everything; today I'd split persistence from UI state and probably model the save status with a real state-machine library instead of nested setTimeouts. The preview renders with dangerouslySetInnerHTML straight off marked — fine for notes you write yourself, but it should pass through a sanitizer like DOMPurify before I'd trust it with anyone else's markdown. And the rendered text leans on word-break: break-all, which chops words mid-letter; overflow-wrap would break only when it has to. Small things — but they're precisely the kind of detail the rest of my work has since become about.