Skip to content

Application : Task 3 GPA Calculator OverHaul/Remade#511

Open
RishabhSinha2009 wants to merge 1 commit into
stevenson-space:mainfrom
RishabhSinha2009:Task-3-GPA-Calculator
Open

Application : Task 3 GPA Calculator OverHaul/Remade#511
RishabhSinha2009 wants to merge 1 commit into
stevenson-space:mainfrom
RishabhSinha2009:Task-3-GPA-Calculator

Conversation

@RishabhSinha2009
Copy link
Copy Markdown

@RishabhSinha2009 RishabhSinha2009 commented Apr 1, 2026

Proper Description is on my Application : rsinha27@students.d125.org.

While most students use stevenson space to check the time or see what the food options are, the GPA calculator is a primary reason people visit the site especially during the high stress periods of course selection,finals week and in general. I have remade the calculator to function as a 4 Year Transcript Planner format that is similar to the Infinite Campus layout. By moving away from one course calculations that are very tedious and easy to mess up, I’ve introduced dedicated 'Add School Year' and 'Add Summer' buttons that allow students to instantly build out a chronological roadmap of their entire high school career GPA. This new layout is significantly more user friendly as it provides a 'Full Year' course sync that automatically applies consistent academic weights throughout the entire year's sequence. Whether a student is mapping out a full next year or seeing what their GPA is going to be at the end of the semester, it will be 100% accurate and being quicker and more understandable to use.

Note: I wrote all the JavaScript code and wrote almost all of the code that was in Vue , but used AI to tidy up some spots where I was unfamiliar and to help polish the SASS styling with places I was struggling with. Addiotnally, any extremely hard debug errors to point me in the right direction without using their suggested code so I could learn and write on my own.

Summary by CodeRabbit

  • New Features

    • Added hierarchical year and semester organization for courses
    • Full-year course support spanning both semesters
    • Automatic course level detection from course names
  • Refactor

    • Restructured course storage and GPA calculation system
    • Improved data persistence and recovery mechanisms

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Walkthrough

The GPA Calculator component underwent a major structural refactor, transitioning from a flat course array to a hierarchical year/semester/course model. GPA calculation logic was rewritten to support the new structure, persistence changed from a deep watcher to explicit save/load operations, and support for full-year linked courses was added with automatic input normalization and level detection.

Changes

Cohort / File(s) Summary
GPA Calculator Restructuring
src/views/GpaCalculator/GpaCalculator.vue
Replaced flat courses array with hierarchical years[] -> semesters[] -> courses[] structure. Rewrote GPA calculation from calculateSemesterGrades to calculateAll with normalization of grade/level fields and derived field updates (finalGrade, unweightedGPA, weightedGPA). Changed Course model constructor and properties (id/hasFinalinstanceId/linkedId). Transitioned persistence from EBRCourses deep watcher to explicit SHS_PLANNER_DATA with saveData()/loadSavedData(). Added full-year course support with linked course synchronization, automatic level detection, and input normalization logic. Updated UI flow for year/semester block and course management.

Sequence Diagram

sequenceDiagram
    participant User
    participant Component as GPA Calculator
    participant Calc as Calculation Engine
    participant Storage as Local Storage
    
    User->>Component: Update course (name/grade/level/weight)
    Component->>Component: normalizeGrade() & normalizeLevel()
    
    alt Full-Year Course?
        Component->>Component: syncLinkedCourses()<br/>(update paired semester)
    end
    
    Component->>Calc: calculateAll()
    Calc->>Calc: Iterate years→semesters→courses
    Calc->>Calc: Recompute weighted/unweighted GPA
    Calc->>Component: Return updated hierarchy
    
    Component->>Component: Update derived fields<br/>(finalGrade, GPA values)
    Component->>Storage: saveData() to SHS_PLANNER_DATA
    Storage->>Storage: Persist with schema validation
    Component->>User: UI re-renders with new values
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 From flatness rose a tree so tall,
With years and semesters stacked through all,
Each course now knows its linked twin pair,
While GPAs calculate with care—
A grand restructuring, complete and fair! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Application : Task 3 GPA Calculator OverHaul/Remade' is partially related to the changeset. It correctly identifies the GPA Calculator component as the subject, but uses informal phrasing ('OverHaul/Remade') that doesn't clearly convey the specific architectural change from a flat course structure to a hierarchical years/semesters/courses model.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/views/GpaCalculator/GpaCalculator.vue (4)

466-496: Plain objects cast as Course won't be class instances.

The loaded course data is a plain object cast with as Course, not an actual Course class instance. Currently this works because Course only has properties, but if methods are added to the class later, they won't be available on loaded data.

Additionally, lines 483-490 preserve unweightedGPA, weightedGPA, and finalGrade from storage, but calculateAll() at line 508 immediately recalculates and overwrites these values, making the preservation redundant.

Consider using createCourse or Course constructor
-                      ? sem.courses.map((course: any) =>
-                        ({
-                          instanceId:
-                            typeof course.instanceId === 'string'
-                              ? course.instanceId
-                              : this.createId(),
-                          linkedId:
-                            typeof course.linkedId === 'string'
-                              ? course.linkedId
-                              : null,
-                          name: typeof course.name === 'string' ? course.name : '',
-                          grade: this.normalizeGrade(course.grade),
-                          level: this.normalizeLevel(course.level),
-                          weight:
-                            course.weight === 1.5 || course.weight === 1.0
-                              ? course.weight
-                              : 1.0,
-                          unweightedGPA:
-                            typeof course.unweightedGPA === 'number'
-                              ? course.unweightedGPA
-                              : 4.0,
-                          weightedGPA:
-                            typeof course.weightedGPA === 'number'
-                              ? course.weightedGPA
-                              : 4.0,
-                          finalGrade:
-                            typeof course.finalGrade === 'string'
-                              ? course.finalGrade
-                              : 'A',
-                        } as Course))
+                      ? sem.courses.map((course: any) => {
+                          const c = this.createCourse(
+                            typeof course.name === 'string' ? course.name : '',
+                            typeof course.linkedId === 'string' ? course.linkedId : null
+                          );
+                          c.instanceId = typeof course.instanceId === 'string'
+                            ? course.instanceId
+                            : c.instanceId;
+                          c.grade = this.normalizeGrade(course.grade);
+                          c.level = this.normalizeLevel(course.level);
+                          c.weight = course.weight === 1.5 ? 1.5 : 1.0;
+                          return c;
+                        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/GpaCalculator/GpaCalculator.vue` around lines 466 - 496, The loaded
course objects are plain objects cast as Course which won't provide class
methods later and you also preserve unneeded fields (unweightedGPA, weightedGPA,
finalGrade) that are immediately recomputed by calculateAll; instead, construct
real Course instances via the existing factory/constructor (use
createCourse(...) or new Course(...)) passing instanceId (generate with
this.createId() when missing), linkedId, name, normalized grade/level (use
this.normalizeGrade/normalizeLevel) and weight, and omit loading stored
GPA/finalGrade values so calculateAll can recompute them; update the mapping
that builds courses to return created Course instances rather than plain
objects.

967-973: Hardcoded background: white may not support theming.

Several places use hardcoded white (lines 971, 1042, 1115) instead of CSS variables like var(--background). If the application supports or plans to support dark mode or theming, consider using CSS variables for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/GpaCalculator/GpaCalculator.vue` around lines 967 - 973, The CSS
uses hardcoded "background: white" (e.g., in the .year-block rule and two other
selectors around the same area) which breaks theming; update those rules to use
a CSS variable like "background: var(--background, white)" or a semantic token
such as "var(--card-background, white)" so dark mode and custom themes inherit
correctly—locate the declarations setting "background: white" (including the
.year-block rule and the other selectors at the referenced spots) and replace
the literal with the chosen var() fallback form.

385-388: Inconsistent case handling for "AP" detection.

The AP detection uses the original-case trimmed string while other level keywords use the lowered string. This means "ap chemistry" won't be detected, only "AP Chemistry".

Consider using case-insensitive matching for consistency:

-      if (/\bhonors\b/.test(lowered) || /\bhonor\b/.test(lowered) || /\bAP\b/.test(trimmed))
+      if (/\bhonors\b/.test(lowered) || /\bhonor\b/.test(lowered) || /\bap\b/i.test(lowered))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/GpaCalculator/GpaCalculator.vue` around lines 385 - 388, The AP
check is using the original-case variable `trimmed` while other checks use
`lowered`, causing "ap chemistry" to miss; update the AP detection in the same
if block that references `lowered` and `trimmed` to use a case-insensitive test
(either test `lowered` for `\bap\b` or use a case-insensitive regex like
`/\bAP\b/i`) so AP is detected regardless of case; modify the condition in the
GpaCalculator component where `if (/\bhonors\b/.test(lowered) ||
/\bhonor\b/.test(lowered) || /\bAP\b/.test(trimmed))` appears to use one of
these two consistent approaches.

174-177: Duplicate ID generation logic.

The Course constructor (line 176) and createId() method (line 294) use identical logic for generating IDs. Consider having the constructor call createId() or making ID generation a static method.

Refactor suggestion
   constructor(name = '', linkedId: string | null = null)
   {
-    this.instanceId = Math.random().toString(36).substring(2, 11);
+    this.instanceId = ''; // Will be set by createCourse
     this.linkedId = linkedId;

Then in createCourse:

   createCourse(name = '', linkedId: string | null = null): Course
   {
-    return new Course(name, linkedId);
+    const course = new Course(name, linkedId);
+    course.instanceId = this.createId();
+    return course;
   }

Also applies to: 292-295

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/GpaCalculator/GpaCalculator.vue` around lines 174 - 177, The Course
constructor currently duplicates the ID generation logic found in createId();
refactor so ID generation is centralized: either have the constructor call the
existing createId() method (e.g., replace the inline Math.random... with
this.instanceId = this.createId()) or extract the logic into a static helper
(e.g., Course.generateId()) and use that from both the constructor and
createCourse; update createCourse to use the same centralized generator
(createId() or the new static) and remove the duplicated Math.random... logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/views/GpaCalculator/GpaCalculator.vue`:
- Around line 767-800: removeCourse currently only removes the specified course,
leaving its full-year partner orphaned when course.linkedId is set; update
removeCourse to, after finding and splicing the target course from
semester.courses, check if course.linkedId exists and if so locate the linked
course by instanceId === course.linkedId across the other semester (the
companion semester in the same year), remove that linked course as well, and
then call renumberSemesterCourses for both semesters (use
renumberSemesterCourses(year, sIdx) and renumberSemesterCourses(year,
otherSIdx)) before calling calculateAll so numbering stays correct and no
orphaned linked courses remain; ensure you still handle missing year/semester
and indexes as current code does.
- Around line 36-38: The UI text inside the <div class="edit-note"> contains a
Unicode replacement character (�); open the template where the edit-note div is
rendered (GpaCalculator.vue) and replace the corrupted characters with the
intended punctuation (e.g., em-dash — or explicit bullets "•" or separate
sentences) so the sentence reads cleanly (e.g., "Course names are editable and
can automatically set credit level from the name — full year course is different
from a one semester course — matches Infinite Campus layout"); save the file as
UTF-8 to prevent reintroducing encoding corruption.

---

Nitpick comments:
In `@src/views/GpaCalculator/GpaCalculator.vue`:
- Around line 466-496: The loaded course objects are plain objects cast as
Course which won't provide class methods later and you also preserve unneeded
fields (unweightedGPA, weightedGPA, finalGrade) that are immediately recomputed
by calculateAll; instead, construct real Course instances via the existing
factory/constructor (use createCourse(...) or new Course(...)) passing
instanceId (generate with this.createId() when missing), linkedId, name,
normalized grade/level (use this.normalizeGrade/normalizeLevel) and weight, and
omit loading stored GPA/finalGrade values so calculateAll can recompute them;
update the mapping that builds courses to return created Course instances rather
than plain objects.
- Around line 967-973: The CSS uses hardcoded "background: white" (e.g., in the
.year-block rule and two other selectors around the same area) which breaks
theming; update those rules to use a CSS variable like "background:
var(--background, white)" or a semantic token such as "var(--card-background,
white)" so dark mode and custom themes inherit correctly—locate the declarations
setting "background: white" (including the .year-block rule and the other
selectors at the referenced spots) and replace the literal with the chosen var()
fallback form.
- Around line 385-388: The AP check is using the original-case variable
`trimmed` while other checks use `lowered`, causing "ap chemistry" to miss;
update the AP detection in the same if block that references `lowered` and
`trimmed` to use a case-insensitive test (either test `lowered` for `\bap\b` or
use a case-insensitive regex like `/\bAP\b/i`) so AP is detected regardless of
case; modify the condition in the GpaCalculator component where `if
(/\bhonors\b/.test(lowered) || /\bhonor\b/.test(lowered) ||
/\bAP\b/.test(trimmed))` appears to use one of these two consistent approaches.
- Around line 174-177: The Course constructor currently duplicates the ID
generation logic found in createId(); refactor so ID generation is centralized:
either have the constructor call the existing createId() method (e.g., replace
the inline Math.random... with this.instanceId = this.createId()) or extract the
logic into a static helper (e.g., Course.generateId()) and use that from both
the constructor and createCourse; update createCourse to use the same
centralized generator (createId() or the new static) and remove the duplicated
Math.random... logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b50215c5-57df-4983-aeb6-4d837199f63c

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2a065 and ef11750.

📒 Files selected for processing (1)
  • src/views/GpaCalculator/GpaCalculator.vue

Comment on lines +36 to +38
<div class="edit-note">
Course names are editable and can automatically set credit level from the name � Full year course is different from a one semester course � Matches Infinite Campus Layout
</div>
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.

⚠️ Potential issue | 🟡 Minor

Corrupted character in UI text.

Line 37 contains a Unicode replacement character () which likely indicates encoding corruption. The intended character (possibly an em-dash or bullet) was not preserved.

-          Course names are editable and can automatically set credit level from the name � Full year course is different from a one semester course � Matches Infinite Campus Layout
+          Course names are editable and can automatically set credit level from the name • Full year course is different from a one semester course • Matches Infinite Campus Layout
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="edit-note">
Course names are editable and can automatically set credit level from the name Full year course is different from a one semester course Matches Infinite Campus Layout
</div>
<div class="edit-note">
Course names are editable and can automatically set credit level from the name Full year course is different from a one semester course Matches Infinite Campus Layout
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/GpaCalculator/GpaCalculator.vue` around lines 36 - 38, The UI text
inside the <div class="edit-note"> contains a Unicode replacement character (�);
open the template where the edit-note div is rendered (GpaCalculator.vue) and
replace the corrupted characters with the intended punctuation (e.g., em-dash —
or explicit bullets "•" or separate sentences) so the sentence reads cleanly
(e.g., "Course names are editable and can automatically set credit level from
the name — full year course is different from a one semester course — matches
Infinite Campus layout"); save the file as UTF-8 to prevent reintroducing
encoding corruption.

Comment on lines +767 to +800
removeCourse(yIdx: number, sIdx: number, course: Course)
{
const year = this.years[yIdx];

if (!year)
{
return;
}

const semester = year.semesters[sIdx];

if (!semester)
{
return;
}

const index = semester.courses.findIndex(
(c) => c.instanceId === course.instanceId
);

if (index === -1)
{
return;
}

semester.courses.splice(index, 1);

if (!course.linkedId)
{
this.renumberSemesterCourses(year, sIdx);
}

this.calculateAll();
},
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.

⚠️ Potential issue | 🟠 Major

Removing a full-year course orphans its linked pair.

When a course with a linkedId is removed, the linked course in the other semester is not removed. This leaves an orphaned course with a linkedId pointing to nothing, breaking the full-year pairing behavior.

Consider also removing the linked course:

Proposed fix to remove linked course
       semester.courses.splice(index, 1);

-      if (!course.linkedId)
-      {
-        this.renumberSemesterCourses(year, sIdx);
-      }
+      if (course.linkedId)
+      {
+        // Remove linked course from other semesters
+        year.semesters.forEach((sem, semIdx) => {
+          if (semIdx !== sIdx) {
+            const linkedIdx = sem.courses.findIndex(c => c.linkedId === course.linkedId);
+            if (linkedIdx !== -1) {
+              sem.courses.splice(linkedIdx, 1);
+              this.renumberSemesterCourses(year, semIdx);
+            }
+          }
+        });
+      }
+
+      this.renumberSemesterCourses(year, sIdx);

       this.calculateAll();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
removeCourse(yIdx: number, sIdx: number, course: Course)
{
const year = this.years[yIdx];
if (!year)
{
return;
}
const semester = year.semesters[sIdx];
if (!semester)
{
return;
}
const index = semester.courses.findIndex(
(c) => c.instanceId === course.instanceId
);
if (index === -1)
{
return;
}
semester.courses.splice(index, 1);
if (!course.linkedId)
{
this.renumberSemesterCourses(year, sIdx);
}
this.calculateAll();
},
removeCourse(yIdx: number, sIdx: number, course: Course)
{
const year = this.years[yIdx];
if (!year)
{
return;
}
const semester = year.semesters[sIdx];
if (!semester)
{
return;
}
const index = semester.courses.findIndex(
(c) => c.instanceId === course.instanceId
);
if (index === -1)
{
return;
}
semester.courses.splice(index, 1);
if (course.linkedId)
{
// Remove linked course from other semesters
year.semesters.forEach((sem, semIdx) => {
if (semIdx !== sIdx) {
const linkedIdx = sem.courses.findIndex(c => c.linkedId === course.linkedId);
if (linkedIdx !== -1) {
sem.courses.splice(linkedIdx, 1);
this.renumberSemesterCourses(year, semIdx);
}
}
});
}
this.renumberSemesterCourses(year, sIdx);
this.calculateAll();
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/GpaCalculator/GpaCalculator.vue` around lines 767 - 800,
removeCourse currently only removes the specified course, leaving its full-year
partner orphaned when course.linkedId is set; update removeCourse to, after
finding and splicing the target course from semester.courses, check if
course.linkedId exists and if so locate the linked course by instanceId ===
course.linkedId across the other semester (the companion semester in the same
year), remove that linked course as well, and then call renumberSemesterCourses
for both semesters (use renumberSemesterCourses(year, sIdx) and
renumberSemesterCourses(year, otherSIdx)) before calling calculateAll so
numbering stays correct and no orphaned linked courses remain; ensure you still
handle missing year/semester and indexes as current code does.

@joshuaf745
Copy link
Copy Markdown
Contributor

From application review:

Open-Ended Task

  • There are unicode replacement characters on the GPA calculator page; was this not checked at all before submitting? CodeRabbit also caught this: [link](Application : Task 3 GPA Calculator OverHaul/Remade #511 (comment)), and it's extremely visible when you open the GPA Calculator page, so I would've liked to see this fixed.
  • The way what groups are already added is tracked seems to be a little broken, for example, if you add all four years, then delete freshman and sophomore years, you still get the option to add senior year. Same issue with summer school.
  • I think I would've liked a way to move classes between semesters and groups/years.
  • The bottom padding of the course card seems a little broken.
  • I like how the course details are synced between the two semesters but a visual indicator that it was being synced (maybe an icon and a way to disable) would be nice.
  • If you add a one semester course followed by a full year course, they display on different lines. If this wasn't intentional, I think it would've made more sense to have a placeholder thing.
  • The limit of seven courses is wrong, since it's possible to take more with online classes. I think the limit should just be removed.

Evaluation

  • Overall, I really like the implementation of this feature. There's some quirks with how the linking and display works but that can be fixed.
  • I'm a little concerned by the unicode replacement characters in the text, since they're very obvious and I'm surprised that they weren't caught. I'm a little concerned that this wasn't checked after implementation.

@joshuaf745 joshuaf745 self-assigned this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants