GPA Calculator Project Report
1. The Concept
GPA Calculator is a free, mobile-first web app that helps Cambodian university students instantly convert their midterm (out of 40) and semester (out of 60) scores into clear Midterm, Semester 1, Semester 2, and Annual GPA estimates — all without signing up, installing anything, or doing manual math. It supports both Khmer and English and works entirely offline in the browser.
2. The Stack
The Stack: Pure HTML + CSS + vanilla JavaScript in a single index.html file. No framework, no build step, no server required — just open the file in any modern browser. Data is persisted via localStorage. Google Fonts provides Noto Sans Khmer and Outfit for a polished bilingual look.
The AI Assist: Cursor AI (Claude) was used throughout for rapid iteration — generating the initial layout, refactoring the score system from a single-score model to Mid/40 + S/60, debugging localStorage issues, redesigning the UI from long-scroll to tabbed panels, and writing i18n strings in both Khmer and English.
3. The 1 Day Sprint
Morning (Hours 1–3): Set up the project structure and core logic — RUPP 4.0 grade scale, course input rows with name + midterm + semester fields, the GPA calculation engine (percentage conversion, grade lookup, period GPA averaging), and bilingual i18n system (Khmer/English). Built the initial 4-tab layout: Home, Course, Result, More.
Afternoon (Hours 4–6): Split the Course screen into Semester 1 and Semester 2 blocks with synced course names. Redesigned the Result screen with S1/S2 tables plus a Year GPA card. Implemented the full score scale (Mid ÷ 40, S ÷ 60 → % → Avg → Grade → GPA). Added localStorage draft saving, dark mode, and language toggle.
The Finish Line (Hours 7–8): Major UX overhaul to reduce scrolling — converted stacked semester blocks into compact tabs (Sem 1 / Sem 2) on both Course and Result screens. Moved the Calculate button to a fixed bar above the tab navigation. Shrunk row heights, spacing, and font sizes. Replaced the dropdown language selector with a compact ខ្មែរ | EN pill toggle in the header and settings. Removed redundant elements (Recent list, summary card, hint text) to keep everything above the fold on mobile.
4. The Roadblocks
Roadblocks: The biggest bug was t("needMinCourses")(block.count) — the i18n value was a function, so t() returned a function, but the code then called the returned *string* as a function, causing a t(...) is not a function crash. The try/catch around calculateAndShow() masked this by showing the wrong alert ("Enter at least one score") instead of the real error. This made it seem like scores weren't being read from the DOM when they actually were. Fixing required: (1) changing the call pattern to t("needMinCourses", count), (2) hardening load() against corrupted localStorage (non-array courses field), and (3) making the catch block surface real error messages instead of a generic fallback.
5. Key Takeaways
- A single-file architecture (HTML + CSS + JS) keeps deployment trivial but demands discipline — no module boundaries means bugs cascade silently.
- Bilingual i18n with function-based translations (for interpolation) requires consistent calling conventions; mixing
t(key)(arg)andt(key, arg)is an easy trap. - Mobile-first design means measuring scroll depth early — stacking two 5-row semester blocks plus a formula box plus a button exceeds one viewport; tabs solve this elegantly.
localStoragedata can be corrupted by old app versions or user manipulation; always validate withArray.isArray()before.forEach().
My final thought: Building a useful utility app in one sprint is absolutely achievable with AI-assisted development — the key is iterating on real user feedback (screenshots showing broken UI or wrong alerts) rather than guessing what's wrong. Each bug screenshot told us more than any log could.