Skip to content

Repository files navigation

Sticky Notes

A single-page sticky-notes web app. Click and drag on the board to create a note of a chosen size and position; drag notes to move them, drag the corner to resize, and drop a note on the trash zone to delete it. Notes persist across reloads.

Built with Vite + React + TypeScript and no UI or drag-and-drop libraries — the interaction (drag-to-create, move, resize, trash-drop) is engineered by hand on top of plain mouse events, which is the point of the exercise.

Sticky Notes board with several notes of different colors and sizes

Features

All four required behaviors plus four optional bonuses:

  • Create a note of a given size at a given position (click-drag on empty board).
  • Move a note by dragging its body.
  • Resize a note by dragging its bottom-right corner.
  • Delete a note by dragging it onto the trash zone.
  • Bonus: inline text editing.
  • Bonus: bring-to-front on overlap (clicking/selecting a note raises it).
  • Bonus: persistence across reloads (mock async REST API over localStorage).
  • Bonus: five note colors.

Quick start

Requires Node.js 18+ and pnpm.

pnpm install         # install dependencies
pnpm dev             # start the dev server (http://localhost:5173)
pnpm test            # run the test suite once
pnpm build           # type-check + production build
pnpm exec tsc -b     # type-check only

Browser support

Desktop only, minimum resolution 1024×768. Targets the latest Chrome, Firefox, and Edge (Windows/macOS).

Architecture

State is centralized in a pure reducer. All note state transitions live in src/state/notesReducer.ts, implemented as a pure function with no DOM access and no side effects. Every feature — add, move, resize, delete, edit text, bring-to-front, load — is a reducer action, never an ad-hoc mutation scattered across components. Keeping the reducer pure makes it trivial to unit-test in isolation (8 tests cover every action and its edge cases) and keeps the single-source-of-truth guarantee honest: components dispatch intents, the reducer decides the next state.

Dragging and resizing are hand-rolled, and tuned for performance. Two custom hooks, useDraggable and useResizable, listen for mousemove/mouseup on window (not the dragged element) so a fast cursor can't outrun the gesture. The load-bearing decision is that intermediate drag frames live in local component state, and the global reducer is dispatched exactly once — on mouseup. Without this, every mousemove would re-render the entire notes list. Combined with React.memo on the StickyNote component and stable callbacks, moving one note does not re-render its siblings. Trash deletion is also hand-rolled: while a note is dragged, its board-local rectangle is tested for overlap with the trash zone's getBoundingClientRect() each frame, so the zone highlights on hover and the note is deleted only on drop.

Persistence is isolated behind one seam. src/services/notesApi.ts is the only module that touches localStorage, and it exposes a small asynchronous API (fetchNotes / saveNotes) with simulated latency and a configurable failure rate. Because the rest of the app talks only to that interface, swapping in a real backend means rewriting that one file and nothing else. App loads notes once on mount, mounts the interactive board only after the load resolves (so a note created during the load window can't be silently overwritten), and debounces saves after each change, surfacing load/save status in the toolbar.

Project structure

src/
  types/note.ts            Note domain types + color palette + min dimensions
  state/notesReducer.ts    pure reducer (single source of truth)
  services/notesApi.ts     mock async REST API over localStorage (persistence seam)
  hooks/
    useDraggable.ts        drag-to-move
    useResizable.ts        drag-to-resize
  utils/geometry.ts        rectangle overlap / board-local coordinate helpers
  components/
    Board.tsx              create-by-drag, trash-overlap detection, wiring
    StickyNote.tsx         memoized note (move/resize/edit/delete)
    TrashZone.tsx          drop target with active highlight
  __tests__/
    notesReducer.test.ts   8 reducer unit tests
    StickyNote.test.tsx    3 component tests (incl. an end-to-end drag)
  App.tsx                  reducer + persistence + color toolbar + status
  index.css                global styles
  main.tsx                 Vite entry

Testing

pnpm test runs 11 tests (Vitest + React Testing Library + jsdom): 8 reducer unit tests and 3 StickyNote component tests, including one that simulates a full drag (mouseDownmouseMove on windowmouseUp) and asserts the exact committed position, which is what proves the drag math.

Notes

  • No undo/redo.
  • Fixed-size canvas (no panning/zoom), min-width: 1024px per the brief.
  • The mock API's SIMULATED_FAILURE_RATE defaults to 0; set it above zero locally to exercise the load/save error states already wired into the toolbar.
  • Trash detection recomputes getBoundingClientRect() on each drag frame — cheap at this scale, the kind of thing to revisit only at hundreds of notes.

About

A single-page sticky-notes web app. Click and drag on the board to create a note of a chosen size and position

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages