Coffeeroasters

A three-page site for a fictional coffee subscription service, built around a five-step plan builder that prices itself as you answer.

Type

Portfolio

Role

Developer

Year

2026

Tags

React, TypeScript, Vite, Tailwind CSS, React Router, Headless UI, Netlify

A Frontend Mentor challenge: a marketing site for a fictional coffee subscription service, with a five-step plan builder at its center. Pick a brew method, bean type, quantity, grind, and delivery frequency, and the page assembles your order into a plain-English sentence and prices it per shipment and per month. Built with React 19 and TypeScript on Vite, styled with Tailwind v4.

Features

  • Three routes — Home, About Us, and Subscribe — each setting its own document title and meta description from a React Router loader
  • Five-step plan builder as a set of accordions, opened and answered in any order
  • Sticky progress sidebar that marks each step complete as it’s answered and links down to it
  • Order summary sentence that builds as you go, with ____ standing in for anything unanswered
  • Per-shipment and per-month pricing derived from the quantity and frequency you pick
  • “Create your plan” stays disabled until every required step is answered; confirming opens a modal with the final summary and monthly total
  • Accordions, radio groups, the modal, and the mobile menu are Headless UI primitives, so keyboard navigation and focus rings come from the library rather than being bolted on
  • Self-hosted Barlow and Fraunces variable fonts, with custom sm/md breakpoints matching the challenge’s three designs

Architecture notes

  • Builder state is one reducer, not five pieces of component state. Every selection dispatches into a single useReducer behind a context provider (src/context/SubscriptionContext.tsx). The action union is typed, and the reducer’s default case assigns to a never so adding an action without handling it fails to compile.
  • Derived values live in pure selectors. Completion checks, pricing, and the summary sentence are plain functions over state in subscriptionSelectors.ts — nothing derived is stored, so there’s no second copy to keep in sync.
  • Pricing is a lookup table, not a conditional chain. A Record<Quantity, Record<DeliveryFrequency, number>> maps the nine size/cadence combinations to a price, with a separate multiplier map turning per-shipment into per-month.
  • The capsule rule is enforced in state, not just styling. Capsules can’t be ground, so choosing Capsule clears any grind selection, disables that step, and drops the grind clause from the summary sentence — and the reducer ignores SET_GRIND outright while it’s active, so the UI isn’t the only thing holding that invariant.
  • One generic component renders all five steps. PreferenceQuestion<T> takes the action it dispatches as a type parameter and resolves its own value type from the action union, so each step’s radio group is typed to exactly the options that step accepts.
  • Design tokens go in Tailwind’s @theme. The palette, breakpoints, and font families are declared once as theme variables, with eight text-preset-* component classes covering the type scale from the design file.