Feature/course ratings - #257
Open
mgooding-dev wants to merge 19 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.