← Design engineering
Implementation storyHanlun e-Learning2025–2026Demos reconstructed with synthetic data

From a working prototype to merged UI

On Hanlun’s DSE practice platform I designed the student and teacher apps, built a working Next.js prototype of them, and then contributed frontend code to the production React and TypeScript apps. This page follows that path through three of my merged pull requests. The product side is in the Hanlun product case.

Summary

The prototype was where behaviour got tried; the production apps were where my pull requests landed, merged by the team’s engineers. Each change below names the problem, my scope, what it was built on, the states it had to handle, how it was checked, and its trade-off.

01 · The problem

Behaviour you can’t judge from a static frame

Paper 1 of the DSE maths exam is answered by hand. On screen that means a writing canvas that inks for a pen or mouse but not for a resting hand, maths that looks like the printed paper, and answers that survive leaving mid-paper. Whether those work is a question of behaviour, so they went into a working prototype early, where they could be tried in a browser rather than imagined from frames.

Try it
DSE Past Paper2003 Maths Paper 100:38:32
Paper 17 / 19 questions

7. Solve x2 − 5x + 6 = 0. (3 marks)

Saved at 14:387/ 19

2 / 7A pen writes. So does a mouse.

Fig. 1 — The Paper 1 answer canvas, reconstructed for this site with the app’s input rule. Write with a mouse or pen; on a touch screen, try a finger.
02 · My role

Between the design file and the codebase

A new feature went from Figma to the prototype, then into the production codebase, where I built its UI and states as Storybook stories and in the student and teacher apps, and opened a pull request for an engineer to review and merge.

  1. MeDesignflows and screens in Figma
  2. MePrototypea working Next.js app, Cursor-assisted
  3. MeBuildUI and states as Storybook stories and in the apps
  4. MePull requestopened by me, with screenshots
  5. TeamReviewscreens reviewed in the thread
  6. EngineerMergeafter CI passes
Fig. 2 — How a new feature reached the production codebase, with who does each step.
Pull requests
92 opened and 81 merged, February to October 2026, merged by three engineers on the team.
Screen review
In the three pull requests below, screens were discussed from screenshots in the thread, and requested changes landed as follow-up commits before merge.
Where my code went
Mostly the student and teacher apps, their shared component package and Storybook; a few small changes to the API alongside them.
03 · Prototype to production

Two codebases, kept apart

The prototype was a single Next.js app with the student and teacher views switched by URL. Production splits them into two Vite apps that share a component package and an API client generated from the backend’s schema.

Prototype
Next.js 16, React 19, Tailwind CSS, Radix UI. December 2025 to September 2026.
Production
React 19 on Vite 7, TypeScript 5.9, Tailwind CSS 4, Radix UI and shadcn/ui, TanStack Router and Query, Vitest, Storybook 10, over a Python backend.
Shared
A component package used by both apps, so a reminder or a lesson card is written once; and a typed API client, regenerated when the backend adds a field.
Afterwards
The prototype stayed in use as a reference. In September 2026 an alignment audit against the production teacher app, and an inventory of prototype features not yet in production, were committed to the prototype under my name.
04 · Three merged changesIn the code

Three pull requests, reconstructed

Each one below is a merged pull request of mine from 2026. The code and screens are the client’s and stay private, so each demo is reconstructed for this page with synthetic data to show the behaviour the change introduced.

4.1 · Passing rate · Jul 2026

The passing rate an attempt was judged at

Teachers can change a quiz’s passing rate, and a new rate applies only to future attempts. Another engineer built the backend that stores each attempt’s rate at submission. My change exposed that stored rate to both apps and put it beside every attempt.

Reconstructed demo · synthetic data

Rate for future attempts
View as
  1. Attempt 1Fail45%Passing rate 50%8 Jul, 14:05
  2. Attempt 2Pass55%Passing rate 50%11 Jul, 20:30

{ score: 55, passRateAtSubmit: 50 } — the latest attempt as both views receive it. Future attempts are judged at 50%; earlier ones keep their own rate.

Fig. 3 — Change the rate, submit a new attempt, and switch between the student and teacher views; earlier attempts keep their own rate.
Trigger
The ticket asked for each submitted attempt to show the passing rate it was judged at, now that rate changes are future-only.
My scope
The stored rate added to the student and teacher API responses, mapped into the view models, and shown on lesson cards, the attempt list, the result page and the teacher’s quiz tab, with copy in both languages.
Built on
Another engineer’s backend change, which stores the rate at submission and makes rate changes future-only. An engineer regenerated the typed API client before merge.
States
Pass or fail always visible as a pill, never only in a hover tooltip; the rate paired with the submission time; attempts before and after a rate change each showing their own rate.
Checked
View-model tests for both apps map attempts “with their snapshotted passing rate, not today’s rate”, so a later rate change can’t rewrite history on screen. A lesson-card test checks the rate sits inside the score pill with no tooltip. In review a teammate asked for the rate on both result views; a follow-up commit added it. Merged after CI.
Trade-off
One placement everywhere, next to the submission time, instead of an inline chip on some screens and a hover-only tooltip on others.

Reconstructed outline · not Hanlun’s code

How the stored rate reaches the screen

  1. Engineer

    The backend stores the rate on each attempt at submission.

  2. Me

    The student and teacher API responses include it on every attempt summary.

  3. Engineer

    The typed API client is regenerated from the backend schema.

  4. Me

    Each app’s view model maps it onto the attempt, tested against today’s rate.

  5. Me

    The pill and “rate · submission time” render the same way in both apps.

4.2 · iPad reminder · Jun 2026

A reminder only iPads see

On iPad, a resting hand or the system’s Scribble handwriting could interfere with annotating. The change shows a dismissible reminder, on iPad only, on the student’s answer page and the teacher’s marking page.

Reconstructed demo · synthetic content

Paper 1 · Question 3Answering · saved 14:05

Writing with Apple Pencil? In Settings › Apple Pencil, turn on Only Draw with Apple Pencil and turn off Scribble, so a resting hand doesn’t mark the sheet.

iPad: the reminder sits above the sheet on both handwriting pages, in Traditional Chinese or English.

Fig. 4 — Switch device and page, then dismiss the reminder.
Trigger
Annotation interference on iPad. The team settled on asking users to turn on Only Draw with Apple Pencil and turn off Scribble.
My scope
An iPad check, a reminder component in the shared package used by both apps, copy in Traditional Chinese and English, and Storybook stories for the reminder and the teacher’s marking page.
Built on
The existing annotation pages in both apps and their shared component package.
Edge cases
iPadOS Safari reports itself as a Mac, so the check also looks for a touch screen; laptops never see the reminder; the check is safe where there is no browser; a dismissal lasts for the browser session, so it doesn’t come back on every page.
Checked
Approved by an engineer before merge. The wording changed in review, once the team agreed users should also turn off Scribble.
Trade-off
The pull request calls it an interim fix: it asks users to change a device setting instead of changing how the canvas handles touch.
4.3 · Subject themes · Jul 2026

One theme per subject

Each subject has its own brand colour, and a student or teacher can switch between courses. The change set up a theme system: the selected course’s subject picks a palette, and both apps read the same few colour variables.

Reconstructed demo · this page’s own colours

MathsLearning pathMock papersProgress
Unit 4 · Quadratic functions
Start lesson3 of 5 lessonsView past attempts

Maths brand #C9A227 · button text dark: 6.1:1 passes AA (white would be 2.4:1)

Fig. 5 — Pick a subject; the unmapped one falls back to the default, and the contrast check picks the button text.
Trigger
A ticket for subject colour themes in Storybook, which the change carried through to both apps.
My scope
A shared theme provider driven by the course’s subject, seven palettes with tints derived using color-mix, a Storybook theme toolbar, contrast-check stories, and seed data for checking several subjects locally.
Built on
The apps’ existing selected-course state and the shared component package.
Edge cases
A subject with no mapping falls back to the default theme; two reserve palettes stay in Storybook until a subject claims them; switching course changes the colour variables for the whole page.
Checked
The pull request sets out what the change affects and where reviewers should look. Contrast checking led to adjusting one palette to meet WCAG AA. Merged by an engineer.
Trade-off
The colour variables are set for the whole page, so switching course recolours every surface in both apps at once. The pull request lists that reach for reviewers.
05 · AI-assisted

Working with Cursor, and checking what it wrote

Cursor was the AI coding tool on Hanlun. This is AI-assisted development of an ordinary web app; nothing in this case integrates an AI model into the product.

The record
Cursor is credited as co-author on 68 of my 167 merged production commits and on 135 of my prototype commits, including every non-merge commit of mine in the three pull requests above. A co-author credit marks that Cursor was involved in a commit; it doesn’t measure how much of the code it wrote.
What I set
Project rules and skills for it: rules in the prototype, and a skill in the production repository that drafts pull-request descriptions with Storybook preview links.
A correction
Cursor’s built-in browser froze the editor on the heavy Paper 1 canvas screens. A rule committed under my name now blocks it there: the agent finishes with preview links instead, for a person to check those screens in a real browser.
Team checks
Storybook stories for the states, unit tests with the logic, screens discussed in the pull request, and an engineer merging after CI.
Volume
The prototype has 346 commits, 345 of them mine. In production I made 179 commits, 167 merged to main through 81 pull requests. Counts show where the work went, not its quality.
179commits to the production repo, February to October 2026

The pieces every screen is built from, with their tokens and stories.

  1. 0d02d9202/06finalize StatusTag component with serif font and refined colors
  2. 4757d1902/13Submission confirmation UI and design tokens naming
  3. a57954004/17webAdd Paper1 fullscreen view and shared marking toolbar
  4. 61c0a1a07/21web, dbAdd subject-based brand color themes
  5. 9c64d2207/31webAdd status-error and status-submitting tokens for exam PDF status chips

Real commit subjects from my history, ticket numbers removed. Outlined days are when this kind landed; hover a line to find its day.

Fig. 6 — My commits to the production repository, by day, with examples of each kind. Pick a kind to see where it landed.
06 · Takeaways

What carries over

Turning a design into a working prototype brings interaction and implementation constraints to the surface earlier, while they are still cheap to change.

AI-assisted coding speeds up iteration, but the UI and behaviour it produces still need human judgment and checking before they ship.

07 · This site

The portfolio is a working app too

Every interactive figure here is reconstructed in React rather than embedded as a video or image, including the three demos above.

Stack
Next.js 16 App Router, React 19, TypeScript, hand-written CSS.
Access
Cases under NDA sit behind a password checked on the server. A request proxy serves their pages only to unlocked browsers, so locked visitors never receive the cases’ code or data.
Shipping
GitHub Actions runs lint, type checks and a production build on every push; Vercel deploys.
AI-assisted
Built with Claude Code: most of this repository’s commits are co-authored with it.