Files
redefined-designs/docs/superpowers/specs/2026-08-22-orders-page-design.md
T
bermudalambandClaude Opus 5 d65eb7b981
Linting / lint (pull_request) Successful in 1m43s
SonarQube Analysis / sonarqube (pull_request) Failing after 12m40s
Tests / backend-unit (pull_request) Successful in 38s
Tests / frontend-e2e (pull_request) Failing after 8m36s
docs: design for moving Order History onto its own page (#121)
Records why /orders is a page rather than another modal route. The app has both precedents: /account, /login and /register are in MODAL_ROUTES and render over a backdrop, while /cart and /privacy are ordinary pages. Order history is closer to the cart, a list you read rather than a dialog you dismiss.

A modal at /account/orders was the smaller change and was rejected: it inherits the same 700px width and 70vh scroll cap, so it moves the table without giving it anything. Tabs inside the modal were rejected for the same reason, since they fix the scrolling and leave the cramping.

The doc also records the one part of this that is a fix rather than a move. A failed load currently shows a toast and leaves the table empty, and the toast goes away while the empty table does not, so a customer whose request failed sees exactly what a customer with no orders sees. Loading, failed and empty become three distinct states, with a retry on the failed one.

Per-order detail and server-side paging are written down as deliberately out of scope, so that leaving them out reads as a decision rather than an oversight.

Refs #121
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:39:01 -05:00

5.8 KiB

Order History as its own page

Design for #121. Written 2026-08-22.

The problem

The My Account modal accumulated. It now holds a profile line, a name form, two collapsed panels for changing email and password, two consent switches, an order table, and four account controls. Each addition was reasonable on its own; together they are more than a 700px modal with its body capped at 70vh can show without scrolling.

The order table is the piece that fits worst, because it is the only tabular data in there. Tables want horizontal room and get none: scroll={{ x: 'max-content' }} is already on it to stop it widening the modal past a phone viewport, which is a workaround for being in the wrong container rather than a layout choice.

Decision

A real page at /orders.

The app has two precedents and this picks the one that fits. /account, /login and /register are modal routes, listed in MODAL_ROUTES and rendered over a backdrop so Back closes them. /cart and /privacy are ordinary pages in the same Routes block. Order history is closer to the cart: a list you read, not a dialog you dismiss.

Another modal route at /account/orders was considered and rejected. It would be the smallest change, but it inherits the same 700px width and the same 70vh scroll cap, so it moves the table without giving it anything. Tabs inside the modal were also considered and rejected for the same reason: they solve the scrolling and leave the cramping.

What moves

Out of Account.tsx: the divider, the Order History heading, the <Table>, the orders state, the fetchMyOrders effect, and the fetchMyOrders and OrderHistoryItem imports.

Into Account.tsx: a View order history button in the existing control row beside Download my data and Log out.

Into a new frontend/src/customer/Orders.tsx: the same fetchMyOrders() call and the same four columns.

Page shell

Orders.tsx follows Cart.tsx, which is the established shape for a customer-facing page in this app:

  • <Layout style={{ minHeight: '100vh' }}> wrapping a <Header> that carries a Back to Shop button with ArrowLeftOutlined and a <Title level={3}>Order History</Title>.
  • The same auth guard: if (!authLoading && !customer) navigate('/login').
  • Registered as <Route path="/orders" element={<Orders />} /> in the backdrop <Routes> in main.tsx. Deliberately not added to MODAL_ROUTES — that array is what makes a path render as a modal over a backdrop, and adding it there would undo the whole point of this change.

The part that is a fix rather than a move

The current effect is:

if (customer) void fetchMyOrders().then(setOrders).catch(() => message.error('Could not load your orders'));

A failed load shows a toast and leaves orders as []. The toast disappears after a few seconds; the empty table does not. From then on a customer whose request failed sees exactly what a customer with no orders sees, and the interface is asserting something false — that they have never ordered anything.

That was tolerable as one section among several in a modal. On a page whose entire purpose is this table it is the main thing the page says, so the three states become distinct:

State What is shown
Loading A Spin, so an in-flight request is not mistaken for an empty result either
Failed An Alert naming the failure, with a Retry button that re-issues the request
Empty <Empty description="No orders yet"> with a Continue Shopping link, mirroring the empty cart

The retry matters more than the alert. A transient failure otherwise strands the customer on a page that needs a full reload to recover.

Presentation

  • A container capped at 960px and centred, so rows are not stretched across the full width of a wide monitor. 960 rather than the modal's 700: four columns of which one is a free-text item name, with room to add a fifth without another rethink.
  • Amount right-aligned, which is what makes a column of currency readable.
  • Status rendered as a Tag rather than the raw string the modal prints today.
  • scroll={{ x: 'max-content' }} is kept. It is still correct on a phone; it is simply no longer compensating for the container.

Data flow

No backend change. GET /api/customers/me/orders already returns id, processor, amount_cents, status, created_at and item_name, which is every column.

Out of scope

Two things were considered and deliberately left out.

Per-order detail — clicking a row to see line items, address and payment reference. It needs a new endpoint and a second view, and it is a feature rather than a layout fix. Its own issue when there is a reason for it.

Server-side paging and filtering — worth doing once somebody has enough orders to scroll, which is a guess today. Adding it now would mean designing pagination against a table nobody has filled.

Testing

New frontend/tests/e2e/orders.spec.ts:

  • A signed-out visit to /orders redirects to /login.
  • The page renders for a signed-in customer, with its heading and Back to Shop control.
  • The empty state appears when the customer has no orders.
  • Back to Shop returns to the storefront.
  • The View order history control in My Account navigates to /orders.

Two existing tests in account-modal.spec.ts assert that the modal contains the text Order History — the "shows the signed-in account and its settings" test and the phone test. Both must be updated to assert the link, not deleted. Deleting them would remove the only coverage that the account view still offers a route to the orders at all, which is precisely the thing this change could silently break.

Sequencing

This edits Account.tsx, which the #111 account-details work also edits and which is committed on feature/111-account-ui. #111 lands first; this branch rebases onto it. Doing them in the other order means resolving a conflict that need not exist.