Skip to content

Repository files navigation

DISPATCH — Message Board (UI)

A small messaging-app UI built for a frontend take-home. UI only — data, auth, and actions are mocked/static, per the brief. Focus is design fidelity across mobile (390px) and desktop (1440px) breakpoints, matching the provided fe-challenge-specs.html reference.

Stack

  • Vite + React + TypeScript
  • styled-components, with a typed theme (src/styles/theme.ts) holding every design token — colors, type scale, borders, hard offset shadows, spacing, breakpoint
  • No UI library — every component (button, input, select, tag, avatar, etc.) is hand-built
  • No routing library, no data-fetching library, no tests — intentionally, see Decisions

Getting started

npm install
npm run dev      # start dev server (fixed port 5183)
npm run build    # type-check + production build
npm run preview  # preview the production build

Log in with the pre-filled seeded account (ada@dispatch.dev, any password) — auth is mocked, so any input works.

Screens & states built

  • Login — responsive split layout (yellow intro panel + form), controlled inputs, mocked submit
  • Feed — header, sticky filters sidebar (desktop), sticky compose (desktop), message list
  • Compose — textarea with 240-char counter, tag picker (custom dropdown), disabled Post until there's content; posting actually prepends a message to the feed
  • Filters — multi-select tag chips, user select, native date range picker, clear; fully wired against the mock data (tag, user, and date range all filter the visible list live)
  • Message item — avatar/handle/time/body/tag, author-only inline Edit (textarea
    • counter + Save/Cancel) and Delete (inline confirm), both fully interactive
  • Empty state — appears for real whenever the active filters match nothing (try tag RANDOM + user Priya Shah)
  • Loading state — a timer simulates an initial "fetch" (~900ms) so the skeleton placeholders are actually visible on load, not just a static mock; a small "Reload feed" link re-triggers it on demand
  • Pagination — desktop shows an explicit Load more button; mobile scaffolds infinite scroll with an IntersectionObserver sentinel. Both simulate the same loading delay (skeletons appended below the existing list) before revealing the next page

Decisions & judgement calls

Where the reference was silent or ambiguous, here's what I chose and why:

  • NOW reference captured once at load (src/lib/time.ts) — relative timestamps ("2m", "18m", "1h") are offsets from a single fixed point, not a live-updating clock, so they read identically for the whole session regardless of when the app is opened. It's anchored to the real current time (not a fixed historical date) so the Date filter stays intuitive — picking "today" matches the messages that read as recent, instead of a mismatch where "2m ago" secretly means some fixed day in 2026.
  • Multi-select tag filters — the reference showed only one active tag chip at a time, but multi-select (filtering by several tags at once) is more useful and was requested explicitly, so Filters tracks an array of selected tags rather than one.
  • Custom Select component — the "User" filter is a fully custom dropdown (src/components/ui/Select), not a native <select>, so its open state matches the brutalist design system (hard shadow, no OS chrome) instead of falling back to the browser's native listbox styling.
  • Date range filter uses native <input type="date"> — the reference shows plain placeholder text fields with no picker at all. A real (if unstyled) date picker is more useful than a free-text field that's never actually parsed, so "From"/"To" are native date inputs, fully wired against createdAt. The trade-off: the calendar popup is OS-drawn and can't be restyled to match the design system (a real CSS/ browser boundary), but the input box itself keeps our border/font/focus treatment, and each end constrains the other via min/max so the range can't invert.
  • Message tag badges are always neutral (white) — the reference's example messages happened to show a yellow "PRODUCT" badge, but that read as demonstrating the Tag component's "selected" visual state rather than a rule that one specific tag is always accented. Privileging one tag's color permanently in a live list felt arbitrary, so every message's tag badge renders the same (neutral) way.
  • Mobile filters collapse to a chip row + button — the reference's mobile artboard shows only tag chips and a gear icon with no documented behavior. I wired the gear to expand/collapse the User + Date + clear controls inline, so it isn't a dead affordance.
  • Inline edit & delete are fully local UI state — "author-only inline edit and delete affordances" are built as real interactions (textarea swap-in for edit, a Cancel/Delete confirm step in place of a browser confirm()), scoped to component state — there's no backend, so nothing persists across a reload beyond what's already described below for auth.
  • Simulated network delay — there's no real backend, so both the initial load and "Load more" fake a ~900ms request with setTimeout specifically so the loading skeletons have a real moment to appear, rather than only existing as an unreachable static state.
  • Simple auth persistence + routing, no router libraryuseAuth (src/hooks/useAuth) persists the mocked "logged in" flag to localStorage so a page refresh doesn't bounce you back to Login. App.tsx reflects that in the URL (/ vs /login) via the History API directly. A full router (react-router, etc.) felt like exactly the kind of "React/Next.js architecture" the brief asks to leave out for a 2-screen app gated by one boolean.
  • Sticky Filters + Compose on desktop — both pin at the same vertical offset while the message list scrolls beneath them. The sticky Compose wrapper covers the strip above it with the page background (rather than a plain top offset) so scrolled messages don't peek through the gap above the pinned card.
  • Mobile order: Compose → Filters → Messages — matches the reference's mobile artboard exactly. Desktop keeps Filters as a left sidebar; both layouts render from the same three components, repositioned per breakpoint via CSS Grid placement (desktop) vs. plain DOM order (mobile) — no duplicated markup.

Project structure

src/
├── components/
│   ├── ui/          # generic primitives (Button, TextInput, Tag, Avatar, Select)
│   ├── layout/       # AppHeader
│   └── feed/         # feed-specific composites (Filters, Compose, MessageItem, ...)
├── screens/          # Login, Feed
├── hooks/            # useAuth, useDismissableOpen
├── data/             # mock users/tags/messages + types
├── lib/              # time formatting
└── styles/           # theme, global styles, styled-components typing

Every component follows the same convention: Component.tsx + styles.ts + index.ts.

About

dispatch-app

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages