Skip to content

feat(investment-calculator): responsive layout, loading skeleton and error boundary - #1308

Merged
nanaf6203-bit merged 1 commit into
MettaChain:mainfrom
peaceshallom37-rgb:feat/1267-1269-1270-investment-calculator
Oct 21, 2026
Merged

nanaf6203-bit merged 1 commit into
MettaChain:mainfrom
peaceshallom37-rgb:feat/1267-1269-1270-investment-calculator

Conversation

@peaceshallom37-rgb

@peaceshallom37-rgb peaceshallom37-rgb commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What this does

The investment calculator is the investment form of the app. It is rendered on /widget/investment-calculator and embedded in other people's pages through that route, which serves it in an iframe. That means it has to work at a width nobody chose — a 600px column on a desktop, a 320px phone screen on a mobile — and that a failure inside it empties the frame the visitor came for.

This is the widget: src/components/widget/InvestmentCalculatorWidget.tsx.

#1267 — the layout assumed a width it could not know

The widget had a fixed padding, fixed font sizes and a metric grid of repeat(2, 1fr). A 1fr column cannot shrink below the width of its content, so on a narrow frame the numbers were pushed out of the widget.

  • The container fills the frame and respects its box model (width: 100%, max-width: 600px, box-sizing: border-box).
  • The paddings and the font sizes scale with the viewport (clamp()), so the widget shrinks with the frame.
  • The metric columns are minmax(0, 1fr), so they can shrink below their content.
  • The rows that hold a label and a number wrap instead of squeezing them, and a long number breaks inside its own box rather than pushing the layout out.

Checked at 320px, 375px and 600px: the page does not scroll sideways at any of them, and the metrics stay in two columns.

#1269 — the embed page showed an empty box while it was booting

/widget/investment-calculator rendered an empty 400px box until the client had mounted, which read as a broken widget rather than a loading one. It now renders InvestmentCalculatorSkeleton, which has the same padding, the same grid and the same blocks as the widget, so the calculator takes its place without the frame jumping around. It is a role="status" element, so it is announced as well.

#1270 — a failure inside the widget emptied the frame

The widget renders inside somebody else's page, so an error that escaped it left an empty box with no explanation. It is now wrapped in the shared ErrorBoundary (src/components/ui/SimpleErrorBoundary, the same one the gallery and the filter panel use) with a fallback that says what happened and offers a retry. The retry remounts the boundary, which clears the error it caught.

While in there

  • The projection is a pure function now. It lives in src/lib/investmentProjection.ts, is unit tested on its own, and is memoised in the component. It used to be calculated in an effect, which painted a panel of zeroes for one render before the real figures arrived.
  • The inputs are free-form number fields. An empty, zero or negative amount used to be divided by and rendered as NaN% and Infinity mo. The projection is only calculated for a positive amount over a positive holding period, and anything that is not a number is read as zero.
  • Intl.NumberFormat parses the locale data every time it is built, and the widget formatted a dozen values on every render. There is one formatter for the module now.

Tests

src/lib/__tests__/investmentProjection.test.ts                        7 passed
src/components/widget/__tests__/InvestmentCalculatorWidget.test.tsx   15 passed
src/components/widget/__tests__/InvestmentCalculatorWidget.error.test.tsx   2 passed

They cover the projection (including the empty, zero, negative and non-finite inputs), the rendered figures, the recalculation when an input changes, the responsive layout, the skeleton, and a failure inside the widget being caught and turned into the retryable fallback.

docs/investment-calculator-widget.md describes the projection, the layout, the loading state and the error handling.

Notes for the reviewer

There is no InvestmentForm in the repository, so this PR works on the investment form that does exist: the calculator widget on /widget/investment-calculator. Another open PR (#1305) adds a new InvestmentForm component under src/components/forms/; this PR does not touch those files, so the two do not conflict.

The fallback of the error boundary is written in the theme of the widget rather than with the shared one, because the widget is embedded in pages that are not part of this app and the shared fallback does not follow the theme. The retry is implemented by remounting the boundary through its key, which resets the error state; the inputs are therefore back at their defaults after a retry.

The labels of the four inputs are not associated with their fields. That is an accessibility issue of its own and it is tracked separately, so it is left alone here.

CI

The workflow run of this pull request is waiting for approval (action_required), because it comes from a fork. It has to be approved before it starts.

main is currently red for reasons that are not related to this branch: npm run lint reports 82 errors and npm run typecheck reports 472 errors across the repository (missing react-pdf and @types/leaflet typings, for instance), and a set of wallet test suites fail. The same failures are on main and on the other open branches. This branch adds no new lint, type or test failures — eslint and tsc are clean on the files it touches, and the full jest run has the same 17 failing suites as main (all of them pre-existing) with 24 more passing tests.

Closes #1267
Closes #1269
Closes #1270

…or boundary

The investment calculator is embedded in other people's pages through an
iframe, so it has to work at a width nobody chose: a 600px column on a
desktop and a 320px phone screen on a mobile.

MettaChain#1267 - The layout assumed a width it could not know.

The widget had a fixed padding, fixed font sizes and a metric grid of
`repeat(2, 1fr)`. A `1fr` column cannot shrink below the width of its
content, so on a narrow frame the numbers were pushed out of the widget.
The container now fills the frame and respects its box model, the
paddings and the font sizes scale with the viewport, the metric columns
are `minmax(0, 1fr)` so they can shrink, and the rows that hold a label
and a number wrap instead of squeezing them. A long number breaks inside
its own box rather than pushing the layout out.

MettaChain#1269 - The embed page showed an empty box while it was booting.

An empty 400px box read as a broken widget rather than a loading one.
The page now renders `InvestmentCalculatorSkeleton`, which has the same
padding, the same grid and the same blocks as the widget, so the
calculator takes its place without the frame jumping around. It is a
`role="status"` element, so it is also announced.

MettaChain#1270 - A failure inside the widget emptied the frame.

The widget renders inside somebody else's page, so an error that escaped
it left an empty box with no explanation. It is now wrapped in the shared
`ErrorBoundary` with a fallback that says what happened and offers a
retry; the retry remounts the boundary, which clears the error.

While in there:

- The projection is calculated by `calculateInvestmentProjection` in
  `src/lib/investmentProjection.ts`, a pure function that is unit tested
  on its own, and it is memoised in the component. It used to be
  calculated in an effect, which painted a panel of zeroes for one
  render before the real figures arrived.
- The inputs are free-form number fields, so an empty, zero or negative
  amount used to be divided by and rendered as `NaN%` and `Infinity mo`.
  The projection is now only calculated for a positive amount over a
  positive holding period.
- `Intl.NumberFormat` parses the locale data every time it is built, and
  the widget formatted a dozen values on every render. There is one
  formatter for the module now.

Closes MettaChain#1267
Closes MettaChain#1269
Closes MettaChain#1270
@drips-wave

drips-wave Bot commented Oct 20, 2026

Copy link
Copy Markdown

@peaceshallom37-rgb Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@nanaf6203-bit nanaf6203-bit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responsive layout, skeleton, and error boundary round this out nicely. Good to merge.

@nanaf6203-bit
nanaf6203-bit merged commit 362f913 into MettaChain:main Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add error boundary for InvestmentForm Add loading skeleton to InvestmentForm Fix responsive layout in InvestmentForm

2 participants