Skip to content

Update course page | Clean up grades page - #344

Open
neron-png wants to merge 13 commits into
devfrom
new_course
Open

Update course page | Clean up grades page#344
neron-png wants to merge 13 commits into
devfrom
new_course

Conversation

@neron-png

Copy link
Copy Markdown
Member

No description provided.


/** Coefficients are whole numbers of at most two digits. */
export function normalizeCoefficient(coefficient: number | string): string {
if (!coefficient) return String(coefficient ?? '');

export function gradeDistributionChart(canvas: HTMLCanvasElement, statistics: ExamStatistics) {
const barColorPassed = "#004C96";
const barColorFailed = "#A2A7AF"
@neron-png

Copy link
Copy Markdown
Member Author
  • Fix when going to a course and coming back to grades, the scroll loses its position (probably due to the stats card updating height)
  • Finalize styling for dark mode + edge margins and content/book dropdowns
  • Update to use registrations to get accurate lecturer information as well as to precache some information

neron-png and others added 11 commits July 20, 2026 00:27
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes dead code, gives components consistent PascalCase names that say
what they render, and lifts shared types and chart config out of the
components. No UI changes.

Structure:
- Delete newGrades.svelte (empty), flipstore.ts and getSemester.ts (both
  verbatim duplicates of helpers.ts), and grades.svelte (a 24-line {#each}
  indirection, now inlined into GradesPage).
- helpers.ts becomes the shared home for getSemester + the flipped store.
- Chart config moves to grades/charts.ts as a Svelte action, mirroring
  courses/charts.ts. StatsCard loses its afterUpdate/bind:this/manual
  destroy, and no longer rebuilds the whole Chart on every update.
  Kept separate from helpers.ts: chart.js is ~200KB behind a side-effecting
  import, so folding it in would drag it into every helpers consumer.
- SemesterGroup/Registration/AveragesResult were declared in three files;
  they now live in $types/grades.
- Semester state is passed as plain arrays rather than Writable props.

Fixes:
- Custom course ids restarted from 0 on reload while the courses persisted
  in localStorage, so a new course could collide with a restored one and
  break both deletion and the getElementById lookups. nextId is now seeded
  past the highest persisted id, and the {#each} is keyed.
- Flipper created a new store subscription on every reactive re-run and
  never unsubscribed. Uses auto-subscription now.
- inputUpdate assigned to `course` 13 lines before its `var` declaration,
  working only via hoisting. Uses a local const.
- The degree calculator's semester label was hardcoded Greek; it now uses
  getSemester + $t like the grades side.

Also drops unused imports: leaflet/cheerio in the degree card (bad
auto-imports; tree-shaken, so no size change) and an unrendered Card in
recents.svelte.
chart.js ships unregistered and throws at runtime when a chart's
controller was never registered. Only statsCard imported 'chart.js/auto',
so its side effect silently supplied registration for courses/charts.ts,
which imports the bare 'chart.js' and registers nothing itself.

Nothing in the source guaranteed that ordering. coursePage is reachable
from the homepage recents via recentGrades.svelte's navController.push,
with no grades page in the graph, so the course page charts only render
because rollup happened to merge the registration, courses/charts.ts and
averages into one chunk that the homepage pulls in for its GPA display.
A different chunking decision would have broken them.

Both chart modules now import Chart from $lib/chart, which owns the
registration, so each chart carries its own rather than depending on an
unrelated feature having been loaded first.
The calculator read its state back out of the DOM: inputUpdate walked
document.getElementById(course.id) for every row, parsed .value, and wrote
validation styling straight onto .style.borderColor. main/fetchData/
inputUpdate/numberCheck/checkPrecision/degreeGradeUpdate/gradeString each
mutated objects handed in by the caller, which is why the card needed
`degree_grade = degree_grade` three times to make svelte notice.

Those seven files collapse into two: degreeGrade.ts (pure: input masking,
the averages, formatting) and fetchDegreeData.ts (the I/O). The card now
derives the grade reactively from state — `$: degreeGrade =
computeDegreeGrade(passedSums, [...unpassedCourses, ...$customCourses])` —
and inputs use bind:value with class:invalid instead of getElementById and
inline styles. CustomCourseRow takes named handlers rather than the drilled
clickInput/gradeInput pair.

The masking is unusual and load-bearing, so it was pinned down first: a
temporary differential test ran the original numberCheck/checkPrecision
against the replacement over 136 raw/previous combinations, all matching.
degreeGrade.test.ts keeps those expectations, quirks included (85 -> 8.5,
80 -> 8.0, 10.5 -> 1.05, 00 -> 0.0, >= 100 rejected).

Behaviour this changes, all previously broken:
- Custom course edits now persist. bind:value mutated the store's objects
  in place without ever calling set, so nothing was written to localStorage
  until an add or delete happened to trigger it.
- Restored custom courses now count towards the average on load. inputUpdate
  only ran on an input event, so saved predictions were ignored until you
  typed somewhere.
- Input borders no longer reset to a hardcoded #ccc on every keystroke,
  which in dark mode flipped them from the themed colour to light grey.
  The invalid state is a class now, so the CSS variable holds.
- The standalone /degreeCalculator route rendered <Card /> without the
  required flip prop; it now defaults to a no-op.
Its logic lived under lib/functions/degreeCalculator while its components
lived under lib/components/degreeCalculator, so the feature spanned two
trees. Nothing outside the feature imported the logic, which is what
separates it from gradeAverages — that one is genuinely shared with the
homepage and stays put. Moving it alongside the components matches how
courses/ already keeps charts.ts next to coursePage.svelte.

CoursesSkeleton was 13 lines used only as the card's {#await} pending
branch, so it moves inline. That also retires a second component named
CoursesSkeleton, which collided with the unrelated one in courses/.

degreeGrade.ts and fetchDegreeData.ts stay separate: degreeGrade.ts imports
nothing, which is why its tests need no mocking, and fetchDegreeData pulls
in the whole dataService chain.
Flipper is a shared component, but it imported courseAdded from the degree
calculator: the calculator bumped a counter on add so the flipper knew to
re-measure its height. A generic component reaching into one feature for a
signal is the wrong direction, and the counter only covered adds — deleting
a course never re-measured, and the bump ran during the reactive cycle, so
the height could be read before the DOM had the new row.

A ResizeObserver on the two faces covers every cause and runs after layout,
so courseAdded goes away entirely along with the dependency. Height writes
are now guarded on a real change, so the observer can't re-enter.
Pushing the course page over grades makes ionic put .ion-page-hidden
(display: none) on the grades page, so both flip faces measure 0. The
ResizeObserver added in 37f26c6 fired on that and wrote height: 0px,
collapsing the card. With the content that much shorter the scroll
position underneath got clamped, so popping back landed the page at a
different scroll offset than it was left at.

Skip the write when the measurement is 0 and keep the last real height,
so a covered page reappears exactly as it was left.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant