Chapter 05 — Activity Guide

Build a Meeting Note Taker

Learn keyboard shortcuts, the File Download API, and structured data tagging — while building a real tool you'll use every time you plan a show, meeting, or project.

Why This Project?

Every chapter so far has stored data in the browser. This chapter introduces a new idea: getting data out. The File Download API lets you create a file on the fly from a JavaScript string and trigger a browser download — no server, no file system access, no libraries. Just a Blob, a URL, and an anchor click.

The note taker also introduces keyboard shortcuts via addEventListener('keydown'), structured tagging as a data design pattern, and contenteditable for inline text editing — three patterns that show up in nearly every real productivity tool.

File Download API

Create a Blob from a string, wrap it in a temporary object URL with URL.createObjectURL(), attach it to a hidden <a> tag, click it programmatically, then revoke the URL. That's a download with no server.

keydown Events

document.addEventListener('keydown', e => {...}) intercepts every keystroke. Check e.key, e.metaKey, e.ctrlKey to build keyboard shortcuts that work alongside the UI buttons.

Structured Tagging

Each note has a tag field (action / decision / question / idea / note). This turns a flat list into a filterable, exportable structured dataset — without a database.

contenteditable

Add contenteditable="true" to any element to make it inline-editable. Capture the new value on blur or keydown Enter and write it back to your data array.

File Download API Blob + Object URL keydown Events contenteditable Structured Data localStorage Markdown

The AI Angle for This Chapter

This chapter has multiple export formats. That's a good AI challenge — generating three different string formats (plain text, Markdown, actions-only) from the same underlying data array. The key skill is telling AI exactly what structure the output should follow, not just "export the notes."

Format specs beat vague requests. Instead of "export as Markdown," prompt: "Group notes by tag. For each group, write an H2 header, then a bullet list. Action items use GitHub-style checkboxes. Include timestamp in bold before each note text." That's a spec — and AI can execute a spec precisely.

“Exporting data is just building a string with rules.

Before writing any export function, write out the exact output format you want by hand. Paste one real example note through the format and show AI what the output should look like. It will get it right on the first pass.

Before You Build — Map the Data

This app's core is a single array of note objects. Design the data structure before touching the UI:

  1. What fields does a note need? At minimum: id, text, tag, and timestamp. What else might be useful at export time?
  2. Where does the tag live — in the note object or in the DOM element? What happens if you only store it in the DOM?
  3. How will you handle inline editing? When the user clicks a note and edits it, where does that change get written?
  4. The filter only affects what's displayed — not what's stored. How do you make sure filtering never deletes data?
  5. Design the Markdown export format on paper first. What should an actions section look like vs. a decisions section?

Good data design makes every feature easier. If your note object has all the fields it will ever need from the start, none of your export or filter functions require workarounds.

Build It

Phase 1

Scaffold the two-pane layout and note entry box

The layout is a wide main pane (note entry + list) and a narrow sidebar (summary counts, filter, export, shortcuts). The entry box has a tag toolbar above the textarea and an Add button below.

Build a two-column layout: main pane (flexible) and sidebar (340px).
Main pane contains:
- A meeting meta section: title, date/time, attendees, context inputs
- An entry box: tag buttons toolbar (Note/Action/Decision/Question/Idea),
  a textarea for note text, a footer with keyboard hint and Add button
- An empty notes list div
Sidebar contains: summary count cards (Actions/Decisions/Questions/Ideas),
filter buttons, export buttons, keyboard shortcut reference.
Color palette: #111 background, #e00 red, tag colors coded by type.
Phase 2

Build the note data model and render function

Notes live in a JS array. Each has: id (Date.now()), tag, text, time (formatted HH:MM), ts (raw timestamp), done (bool). The render function rebuilds the entire list from the array — newest first — and updates the sidebar count badges.

Write addNote() that:
1. Reads the textarea value and current selected tag
2. Creates a note object: { id, tag, text, time, ts, done: false }
3. Pushes it to the notes array
4. Clears the textarea and calls renderNotes()

Write renderNotes() that:
1. Filters notes by currentFilter ('all' or a tag name)
2. Reverses the visible array (newest first)
3. Builds HTML for each note: tag pill, editable text, timestamp, delete button
4. Action items also get a toggle-done checkbox button
5. Updates count badges in the sidebar for each tag type
Phase 3

Add keyboard shortcuts

Wire up document.addEventListener('keydown') for: Cmd/Ctrl+Enter to submit the note, Escape to clear the textarea, and Ctrl+1 through Ctrl+5 to switch the active tag without touching the mouse.

Add a single keydown listener on document:
- (e.metaKey || e.ctrlKey) && e.key === 'Enter' → call addNote()
- e.key === 'Escape' && activeElement is textarea → clear textarea
- e.ctrlKey && e.key === '1' → setTag('note')
- e.ctrlKey && e.key === '2' → setTag('action')
- e.ctrlKey && e.key === '3' → setTag('decision')
- e.ctrlKey && e.key === '4' → setTag('question')
- e.ctrlKey && e.key === '5' → setTag('idea')
Use e.preventDefault() on shortcuts that conflict with browser defaults.
Phase 4

Build the File Download API exports

Three export functions — plain text, Markdown, and actions-only — each builds a string from the notes array, wraps it in a Blob, creates a temporary object URL, clicks a hidden anchor, and revokes the URL. No server, no file system permissions.

Write exportMd() that:
1. Builds a Markdown string grouping notes by tag
2. Each group has an H2 header and a bullet list
3. Action items use GitHub checkbox syntax: - [ ] or - [x]
4. Each bullet includes timestamp in bold: **[09:02]** Note text
5. Adds meeting metadata (title, date, attendees) at the top

For the download itself:
const blob = new Blob([content], { type: 'text/markdown' });
const url  = URL.createObjectURL(blob);
const a    = document.createElement('a');
a.href = url; a.download = 'meeting-notes.md';
a.click();
URL.revokeObjectURL(url);
Phase 5

Add localStorage persistence and inline editing

Save the entire state (notes array + meeting metadata) to localStorage on every change. On load, restore from storage or load demo defaults. Add contenteditable to note text elements so users can click and edit directly in the list.

Add saveData() that JSON.stringifies { title, date, attendees,
context, notes } into localStorage under key 'it_ch05_notes'.
Call saveData() after every add, delete, toggle, and text edit.

For inline editing:
<div contenteditable="true"
  onblur="updateText(id, this.textContent)"
  onkeydown="if(event.key==='Enter'&&!event.shiftKey){
    event.preventDefault(); this.blur(); }">

The blur fires updateText(id, newText) which finds the note
in the array by id, updates note.text, and calls saveData().

Concepts You Just Used

The Blob download pattern is a superpower. Once you know it, you can export anything from a browser app — CSV, JSON, SVG, Markdown, plain text — without ever touching a server. It's the same technique behind "Export" buttons in Notion, Figma, and Airtable.

Reflect

  1. What's the difference between URL.createObjectURL() and a data: URI? When would you use each?
  2. Why do you call URL.revokeObjectURL() after the download? What happens if you don't?
  3. The filter only hides notes — it doesn't delete them. How is that different from how most filtering systems work in databases vs. in-browser?
  4. Inline editing with contenteditable is powerful but has quirks. What happens if the user pastes rich text (bold, links) into an editable div? How would you prevent that?
  5. This app stores one meeting at a time. What would the data model look like if it stored a history of multiple meetings?

The tools you used here — Blob, Object URL, keydown, contenteditable — are the building blocks behind every browser-based productivity app. You didn't use a library. You used the platform.

Go Further

📄 Meeting History

Store multiple meetings in localStorage as an array keyed by date. Add a "Past Meetings" sidebar panel to browse and reload previous sessions.

🔍 Search

Add a search input that filters the visible notes in real time by matching the note text. Highlight the matched substring in the result.

📋 Copy to Clipboard

Add a "Copy as Markdown" button using navigator.clipboard.writeText() — no file download, just straight to the clipboard for pasting into Slack or Notion.

🕐 Meeting Timer

Add a running elapsed-time counter that starts when the page loads and is stamped into the export header. Bonus: add a "time per section" marker.

👥 Attendee Tracker

Parse the attendees field into a list. Let each note be assigned to a specific attendee. Filter by person and export an individual action list per attendee.

📤 Export to Email Draft

Build the export string and open a mailto: link with the meeting summary in the body — pre-addressed to the attendees field.

Up Next — Chapter 06

Brand Palette Generator

Pick a base color, generate a full brand palette with tints, shades, and complementary colors, and export as CSS variables or a swatch sheet. You'll learn color math, HSL manipulation, and dynamic CSS generation.

Start Chapter 06 →