Skip to content

Feature/course ratings - #257

Open
mgooding-dev wants to merge 19 commits into
ai-hero-dev:mainfrom
mgooding-dev:feature/course-ratings
Open

Feature/course ratings#257
mgooding-dev wants to merge 19 commits into
ai-hero-dev:mainfrom
mgooding-dev:feature/course-ratings

Conversation

@mgooding-dev

Copy link
Copy Markdown

No description provided.

Michael Gooding and others added 19 commits May 13, 2026 14:21
Spec for one-rating-per-enrolled-student system using compute-on-read
averages, a new course_ratings table, ratingService, api.rate-course
route, and StarRating component with display/input modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds courseRatings Drizzle table (id, userId FK, courseId FK, rating,
createdAt) with UNIQUE(userId, courseId) to enforce one-rating-per-student
at the database level. Generated migration 0003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inserts a course rating after verifying enrollment and absence of an existing
rating. Throws on missing enrollment or duplicate submission.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns the user's rating row for a course, or undefined if none exists.
Used by the course detail loader to drive sidebar UI (rate vs. "you rated X★").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns { average, count } for a course via SQL avg/count aggregates.
Average is null when the course has no ratings yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns a Map<courseId, {average, count}> via a single GROUP BY query.
Unrated courses default to (null, 0) so callers can map.get() unconditionally.
Used by the course list loader to avoid N+1 queries per card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validates the form body with Zod, requires an authenticated user (401),
checks enrollment (403), rejects duplicates (400), then submits the rating.
Returns { ok: true } on success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Display mode renders partial-fill stars via stacked outlined + clipped filled
icons, plus numeric average and count. Returns null when count is 0 so cards
without ratings render nothing. Input mode renders 5 submit buttons inside a
Form posting to /api/rate-course, with hover highlight. Supports sm/md sizes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loader bulk-fetches rating stats via getCourseRatingStatsForCourses (one query
for the whole page) and attaches ratingAverage/ratingCount to each card.
CardFooter now stacks a second row with StarRating in display mode; cards
without ratings render nothing (component returns null).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…led students

Loader fetches getCourseRatingStats for the hero display and getRatingForUser
(only when enrolled) for the sidebar conditional. Adds a "Rate this course"
section under "Buy More Seats" that swaps to "You rated X★" once submitted.
Non-enrolled visitors see neither.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The seed script's hardcoded drop list predates the course_ratings table. On
re-seed it dropped every other table and __drizzle_migrations, then migrate()
tried to recreate course_ratings and failed because the orphan table still
existed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plain <Form> caused the browser to navigate to /api/rate-course after submit,
landing the user on a blank JSON response page. fetcher.Form posts in the
background and triggers automatic loader revalidation on the current page,
matching the spec's data-flow ("React Router revalidates courses.$slug loader").
Also disables the buttons while submitting to prevent double-clicks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ratings are no longer immutable. submitRating now upserts via Drizzle's
onConflictDoUpdate on the (userId, courseId) unique constraint, so submitting
again as the same student overwrites the prior value. The API route drops its
duplicate-rate pre-check.

UI: the sidebar now always renders the input stars. They're pre-highlighted at
the user's existing rating when not hovering, and the label switches between
"Rate this course" and "Your rating" based on whether a rating exists.

Spec updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When mousing across the star row, the gap between buttons briefly fell outside
each button's bounding box, firing onMouseLeave (revert to currentRating) then
onMouseEnter on the next button — visible as a flicker. Moving onMouseLeave
to the parent form means the revert only fires when the mouse exits the whole
row, not when crossing between adjacent stars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add lesson_comments and lesson_comment_reports tables, a comment
service with tests, comment UI components, the api/lesson-comments
route, and an instructor moderation page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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