Overview
Students preparing for IELTS submit Task 1 and Task 2 essays, and a tutor corrects them. Before this project, the correction lived in a Google Doc. I moved it into the platform as structured data: highlighted spans with error types, notes and suggestions, four band scores, and overall feedback.
Architecture
API (Laravel). A writing_corrections table stores each span: error type, selected text, UTF-16 start and end offsets, note, and suggestion, anchored per question so Task 1 and Task 2 stay separate. Band scores and overall feedback live on the student's exercise. Tutor endpoints cover create, amend, and delete for spans, plus submit. The student endpoint is owner-scoped and returns an empty shape until the tutor submits.
Tutor app (React). A submissions inbox (pending, corrected, all, with search) and a correction workspace: select text, tag it, add a note and suggestion, then score and submit.
Student app (React). The corrected essay rendered natively, with colour-coded spans per error type. Tapping a span shows the note and the suggested rewrite. Nested spans stay tappable because every covering correction is kept per segment, narrowest first.
Decisions and fixes
The highlight that jumped to the wrong word
In Chrome, a double-click selects a word plus its trailing space. The API's global TrimStrings middleware trimmed selected_text but left the offsets alone, so the stored span no longer matched its own text. Both frontends then re-anchored it to the first occurrence of the word in the essay.
I fixed it on both sides: the tutor app trims the selection range client-side (the offsets move with the text), and the API excludes selected_text from trimming. There's an HTTP-level test that goes through the middleware.
Only the booked tutor corrects
The first version let any tutor correct any essay. That matched an older feature, but not how students actually book: a student books a correction session with a specific tutor. The service now resolves the booking for each essay and applies it on every tutor path, including the inbox. A tutor who opens someone else's essay gets a clear message and a way back to their inbox, not a generic 403 toast.
Drafts never leak
Save Draft stores feedback and whichever bands are set so far, without marking the essay corrected or notifying the student. Once submitted, drafts are refused, so a half-scored card can never reach a student. Submitting also completes the exercise and writes the overall band as its score, so the student no longer sees "Awaiting score" next to "Corrected".
Scoring on the scale
The first scoring UI was four dropdowns of nineteen values each. I replaced it with the ten bands in a row plus a half toggle, and an overall card that says what's still missing. The band-to-controls arithmetic lives in one helper that's tested against every value the API accepts.
Results
- Structured, searchable corrections instead of shared documents
- A tutor inbox that also surfaces essays submitted without a booking
- A native corrected-essay view for students, in all seven live locales
- 38 tests across the tutor writing suites at the access-rule pass, with unit and integration suites green in the student app
