A PWA for managing multiple tea brewing timers with accurate countdowns, visual progress rings, and audio/visual alerts on completion.
Six built-in tea timers, each with a unique accent color:
| Tea | Duration | Color |
|---|---|---|
| Black Tea | 3:00 | Dark navy |
| Green Tea | 2:00 | Forest green |
| Oolong Tea | 4:00 | Saddle brown |
| Jasmine Tea | 2:30 | Sandy orange |
| Earl Grey | 3:30 | Muted purple |
| Matcha | 1:30 | Olive green |
Timers can be customised at runtime via the UPDATE_SETTINGS action, which
accepts partial updates to label, duration, and color.
- Multiple concurrent timers -- all six timers run independently; start, pause, and reset each one individually.
- Web Worker timing -- a singleton Web Worker (
setIntervalat 250 ms) drives all active timers. The main thread computes wall-clock remaining time from an absoluteendTimestamp, so countdowns survive tab switches and device sleep. Falls back to a main-threadsetIntervalwhen the Worker cannot be initialised. - SVG progress ring --
TimerRingrenders a pure SVG ring withstroke-dashoffset-based progress arc.useTimerAnimationprovides 60 fpsrequestAnimationFrameinterpolation between Worker ticks for smooth visual drain. - Screen Wake Lock -- keeps the display on while any timer is running
(browsers supporting the Wake Lock API). Re-acquires the lock on
visibilitychangesince browsers release it when the page is hidden. - Completion alerts
- Audio: A three-tone ascending beep (660 Hz, 880 Hz, 1100 Hz) generated via the Web Audio API -- no external audio files. Plays automatically when a timer reaches zero; autoplay is permitted because the user has already interacted with the page (tapping Start).
- Visual: The finished timer's ring pulses (scale) and glows (drop-shadow) in a repeating CSS animation to attract attention.
- Background resume: If a timer expired while the app was
suspended/backgrounded, the alert sound plays immediately when the app
returns to the foreground and the finished state is detected. This also
covers full page reloads -- timers reconciled to
finishedon load trigger the alert on first render.
- localStorage persistence -- timer state is saved on every change and
reconciled on reload so timers survive page refreshes. Running timers are
reconciled against wall-clock time: if
endTimestampis in the past the timer is markedfinished; if still in the future,remainingSecondsis recomputed. - Long-press gesture -- detected via pointer events with configurable threshold (default 500 ms) and movement tolerance (default 10 px). Cancels gracefully if the pointer moves too far, avoiding accidental triggers during scroll.
- PWA support -- installable as a standalone app with offline caching via
vite-plugin-pwaand WorkboxgenerateSW. Precaches all static assets (JS, CSS, HTML, icons). The service worker auto-updates in the background. Includes a web app manifest with PNG icons (192 px, 512 px), an SVG favicon, and an Apple touch icon (180 px). The viewport disables user scaling for tablet kiosk use.
| Gesture | Action |
|---|---|
| Click / tap | Toggle start / pause |
| Double-click / double-tap | Reset timer to full duration |
| Long-press (500 ms) | Reserved for settings access |
The app displays six tea timers in a responsive CSS Grid:
| Breakpoint | Columns | Layout |
|---|---|---|
| Default (> 768 px) | 3 | 3 x 2 grid |
| Tablet (<= 768 px) | 2 | 2 x 3 grid |
| Mobile (<= 480 px) | 1 | 1 x 6 stacked |
Each timer cell maintains a square aspect ratio. The grid is horizontally
centred with a maximum width of 960 px. The app uses a warm dark theme
(background #2c1e12, text #f0e6d3, title accent #d4a574).
| Layer | File(s) | Purpose |
|---|---|---|
| Entry | src/main.tsx |
Mounts React root, registers service worker via registerSW |
| App Shell | src/App.tsx, src/App.module.css |
Root layout, global styles (warm dark theme), GlobalEffects component for wake lock and alert hooks |
| PWA | vite.config.ts, index.html, public/ |
Manifest, Workbox precaching, icons (SVG + PNG), Apple meta tags |
| Presets | src/data/presets.ts |
DEFAULT_PRESETS array defining the six tea timer configurations |
| Types | src/types/timer.ts, src/types/worker.ts |
TimerConfig, TimerState, TimerAction, WorkerCommand, WorkerTick |
| State | src/context/TimerContext.tsx |
React Context + useReducer, localStorage persistence, wall-clock reconciliation |
| Timing | src/hooks/useTimer.ts, src/workers/timer.worker.ts |
Singleton Web Worker (250 ms tick), main-thread fallback, visibility recovery, reference-counted lifecycle |
| Animation | src/hooks/useTimerAnimation.ts |
60 fps rAF interpolation for smooth ring drain between Worker ticks |
| Ring | src/components/TimerRing/ |
Pure presentational SVG ring with progress arc and finished-state pulse/glow animation |
| Grid | src/components/TimerGrid/ |
Responsive CSS Grid container rendering a TimerCell per timer |
| Cell | src/components/TimerCell/ |
Interactive timer cell: click toggles start/pause, double-click resets |
| Alerts | src/hooks/useTimerAlert.ts, src/utils/alertSound.ts |
Edge-detected audio alert (Web Audio API) on timer finish, with background-resume and page-reload support |
| Wake Lock | src/hooks/useWakeLock.ts |
Screen Wake Lock acquire/release tied to running timer count |
| Gestures | src/hooks/useLongPress.ts |
Long-press detection via pointer events with movement tolerance |
| Formatting | src/utils/formatTime.ts |
mm:ss display formatter |
| Technology | Version | Role |
|---|---|---|
| React | 19 | UI framework |
| TypeScript | 5.7 | Type safety (strict mode, no any) |
| Vite | 6 | Dev server and build tooling |
| vite-plugin-pwa | 0.21 | PWA manifest and Workbox service worker |
| Vitest | 2 | Test runner (jsdom environment) |
| React Testing Library | 16 | Component test utilities |
| @testing-library/jest-dom | 6 | Custom DOM matchers |
npm install
npm run dev # Start Vite dev server
npm run build # Type-check (tsc) + production build
npm run preview # Preview production build locally
npm run lint # ESLint
npm test # Run tests once (Vitest)
npm run test:watch # Watch modeTests use Vitest with jsdom environment and React Testing Library.
Configuration is in vite.config.ts under the test key. A global setup
file (src/test-setup.ts) imports @testing-library/jest-dom/vitest to
provide custom DOM matchers (e.g. toBeInTheDocument).
| Area | Test file | What's tested |
|---|---|---|
timerReducer |
src/context/TimerContext.test.tsx |
All action types (START, PAUSE, RESET, TICK, FINISH, UPDATE_SETTINGS), non-matching id pass-through, reconciliation, persistence |
formatTime |
src/utils/formatTime.test.ts |
Edge cases: 0 s, 59 s, 60 s, large values, two-digit padding, negative input, typical tea durations |
TimerCell |
src/components/TimerCell/TimerCell.test.tsx |
Rendering (label, color, testid, formatted time), all status variants, click to start, click to pause, double-click to reset |
TimerGrid |
src/components/TimerGrid/TimerGrid.test.tsx |
Grid layout and timer rendering |
TimerRing |
src/components/TimerRing/TimerRing.test.tsx |
SVG ring rendering and progress visualisation |
useTimer |
src/hooks/useTimer.test.tsx |
Fake timers, start/pause/reset/toggle, timing accuracy, tick suppression, visibility recovery, Worker lifecycle |
useTimerAlert |
src/hooks/useTimerAlert.test.tsx |
Audio/visual alert triggers |
useTimerAnimation |
src/hooks/useTimerAnimation.test.ts |
rAF-based animation interpolation |
useWakeLock |
src/hooks/useWakeLock.test.tsx |
Wake Lock API management |
useLongPress |
src/hooks/useLongPress.test.ts |
Long-press gesture detection |
alertSound |
src/utils/alertSound.test.ts |
Web Audio API sound generation |
timer.worker |
src/workers/timer.worker.test.ts |
Worker message handling |