Files
babymonitor/frontend_improvements.md

9.8 KiB

Frontend Improvement Report

1. Remove Duplicate .js / .tsx Files — Code Slop

Severity: High

The codebase contains leftover .js files from an incomplete TypeScript migration. Every significant source file has a .tsx/.ts counterpart, making the .js versions dead code that adds confusion and maintenance burden.

Files to delete:

src/api/client.js
src/App.js
src/main.js
src/types/index.js
src/utils/chartHelpers.js
src/utils/time.js
src/components/tracker/SleepButton.js
src/components/tracker/TrackerPage.js
src/hooks/useSettings.js
src/hooks/useSleepSession.js
src/hooks/useStats.js

2. Dead Code — chartHelpers.ts exports never called — Code Slop / Duplication

Severity: High

All 7 exported functions in src/utils/chartHelpers.ts are tested but never imported or called from any component. Components duplicate the same logic inline instead of delegating to these utilities.

Dead exports:

  • buildTimetableSlots() — reimplemented inline in SleepTimetable.tsx
  • computeWakeupMinutes() — reimplemented inline in WakeupPlot.tsx:31-44
  • computeBedtimeMinutes() — reimplemented inline in BedtimePlot.tsx:31-44
  • computeDailySummaryData() — reimplemented inline in DailySummary.tsx:41-48
  • computeAverageStats() — unused
  • reverseDateOrder() — reimplemented inline in multiple chart components
  • filterDaysWithData() — unused

Fix options (pick one):

  • Preferred: Refactor chart components to call the helpers instead of reimplementing — removes ~60 lines of duplicated transformation code.
  • Alternative: Delete the helpers and the tests for them — the logic lives only in the components.

Either way, having tested utilities that the application never actually uses is misleading and inflates the apparent test coverage.


3. Duplicated Datetime Formatting Logic — Duplication

Severity: Medium

toLocalDatetimeInputValue() is implemented in src/utils/time.ts:40 and again locally in src/components/tracker/TimeAdjuster.tsx:8-19. Additionally, src/components/tracker/SessionList.tsx:6-24 defines its own formatDateTime() and toDatetimeLocal() helpers that overlap with what time.ts already provides.

Fix: Consolidate all datetime formatting into src/utils/time.ts. Have TimeAdjuster and SessionList import from there instead of reimplementing.


4. Duplicated Chart Data Transformation — Duplication / Simplification

Severity: Medium

WakeupPlot.tsx:31-44 and BedtimePlot.tsx:31-44 contain near-identical map → reverse → daysAgo pipelines:

// Both files do essentially this:
const chartData = (data ?? [])
  .map((d) => ({
    ...d,
    daysAgo: Math.round((todayMs - new Date(d.date).getTime()) / MS_PER_DAY),
    minutes: adjustForTimezone(d.someTime),
  }))
  .reverse();

The only difference is which field (wakeup_time vs bedtime) is mapped to minutes.

Fix: Extract a single buildTimeSeriesChartData(data, timeField) helper (in chartHelpers.ts) and call it from both components.


5. Loop Inefficiency — Performance

Severity: Low

WakeupPlot.tsx:31-34 and BedtimePlot.tsx:31-34 call new Date(getToday()).getTime() inside the .map() callback, re-creating the same Date object on every iteration.

// Current (creates Date on every iteration)
.map((d) => ({
  daysAgo: Math.round((new Date(getToday()).getTime() - ...) / MS_PER_DAY),
}))

// Fix: hoist above the map
const todayMs = new Date(getToday()).getTime();
const chartData = (data ?? []).map((d) => ({
  daysAgo: Math.round((todayMs - ...) / MS_PER_DAY),
}));

6. any Type Usage — Code Quality / Type Safety

Severity: Medium

Five locations use any, undermining the value of TypeScript:

File Line Issue
src/components/tracker/SleepButton.tsx 19 catch (error: any)
src/components/stats/WakeupPlot.tsx 74, 77 Recharts formatter=(value: any)
src/components/stats/BedtimePlot.tsx 74, 77 Recharts formatter=(value: any)
src/components/stats/DailySummary.tsx 98, 101 Recharts formatter=(value: any, name: any)
src/hooks/useSleepSession.ts 58 payload: any in mutation function

Fixes:

  • Catch blocks: use catch (error: unknown) and narrow with error instanceof Error.
  • Recharts formatters: the Recharts library exports ValueType and NameType — use those, or declare a local type RechartsFormatter = (value: number, name: string) => ReactNode.
  • useSleepSession payload: create a SleepSessionUpdatePayload type in src/types/index.ts and use it.

7. Unsafe User Input Parsing — Security

Severity: Medium

src/components/tracker/SessionList.tsx:40-41 parses datetime-local input values directly without validation:

start_time: new Date(editStart).toISOString(),
end_time: editEnd ? new Date(editEnd).toISOString() : null,

If editStart is an empty string or a malformed value, new Date(editStart) produces an Invalid Date object and .toISOString() throws a RangeError, which bubbles up as an unhandled rejection.

Fix: Validate before submitting:

const parsedStart = new Date(editStart);
if (isNaN(parsedStart.getTime())) {
  // show error to user
  return;
}

8. alert() for Error Messages — Code Slop

Severity: Low

src/components/tracker/SessionList.tsx:47 uses the browser native alert() on save failure:

alert('Failed to update session');

This blocks the UI thread, looks out of place in a modern app, and cannot be styled or dismissed programmatically.

Fix: Replace with a toast notification or an inline error state rendered in the component.


9. Missing Memoisation — Performance

Severity: Low

Several components create new function references on every render, which defeats React's reconciliation optimisations for any child that uses React.memo or useCallback itself:

  • src/components/tracker/SettingsPopover.tsx:72,82,92,102 — four onChange handlers as inline arrow functions
  • src/components/tracker/SessionList.tsx:76,85,91,98,109 — five inline handlers
  • src/components/tracker/TimeAdjuster.tsx:46 — inline onChange
  • src/pages/StatsPage.tsx:44 — inline onChange

Fix: Wrap each with useCallback. For purely presentational components with stable props (IntervalPicker, Navbar, chart components), wrap with React.memo to prevent unnecessary re-renders.


10. No Route-Level Code Splitting — Performance

Severity: Medium

src/App.tsx:14-16 eagerly imports all page components:

import TrackerPage from './components/tracker/TrackerPage';
import StatsPage from './components/stats/StatsPage';

StatsPage pulls in Recharts (a large library) on the initial load even when the user only visits the tracker tab.

Fix:

const StatsPage = React.lazy(() => import('./components/stats/StatsPage'));
// Wrap route in <Suspense fallback={<LoadingSpinner />}>

11. Missing Tests — Testing

Severity: High

Most components have zero unit tests. The ones most at risk:

Component Gap
SessionList Edit flow, validation, delete confirmation
SleepButton Optimistic state, error handling
StatusBanner Conditional rendering, active session display
SettingsPopover Click-outside dismiss, input changes persisted
DailySummary Data transformation rendering
useSleepSession Mutation callbacks, query invalidation

Recommended additions:

  • Unit tests for SessionList edit/save/discard/delete using Testing Library + MSW for API mocks
  • Hook tests for useSleepSession verifying query invalidation after mutations
  • Snapshot or rendering smoke tests for all chart components to catch regressions when data shape changes
  • Test that invalid date inputs in SessionList are rejected before submission

12. Coarse isLoading in useStats — Code Quality

Severity: Low

src/hooks/useStats.ts:32 ORs together three query loading flags:

const isLoading = dailyQuery.isLoading || timetableQuery.isLoading || wakeupQuery.isLoading;

This means consumers cannot show partial data while other queries are still loading — the whole stats page shows a spinner until all three queries complete.

Fix: Either export per-query loading states so consumers can render progressively, or use isFetching instead of isLoading if you want to show stale data while refreshing.


13. Missing Error Boundaries on Chart Components — Reliability

Severity: Medium

There is only one global ErrorBoundary wrapping the whole app. If any chart's data transformation throws (e.g. a malformed date from the API), it takes down the entire stats page.

Fix: Wrap each chart component in its own ErrorBoundary that renders a "Failed to load chart" fallback. This is a two-line change per chart using a reusable boundary component.


Summary Table

# Category File(s) Severity
1 Code slop — dead .js files Multiple High
2 Dead code / duplication chartHelpers.ts High
3 Duplication TimeAdjuster.tsx, SessionList.tsx, time.ts Medium
4 Duplication WakeupPlot.tsx, BedtimePlot.tsx Medium
5 Performance — loop WakeupPlot.tsx, BedtimePlot.tsx Low
6 Type safety — any Multiple Medium
7 Security — unvalidated input SessionList.tsx:40-41 Medium
8 Code slop — alert() SessionList.tsx:47 Low
9 Performance — memoisation SettingsPopover.tsx, SessionList.tsx Low
10 Performance — code splitting App.tsx Medium
11 Testing Most components High
12 Code quality — loading state useStats.ts:32 Low
13 Reliability — error boundaries Stats components Medium