From 1770a6f857d7e17e64dab7c616090f30b3a9b432 Mon Sep 17 00:00:00 2001 From: Henry Kim Date: Wed, 25 Mar 2026 19:17:12 -0700 Subject: [PATCH 01/34] added written review field to rating submission and modified ui to support this --- apps/backend/src/modules/rating/controller.ts | 21 ++- apps/backend/src/modules/rating/resolver.ts | 5 +- .../src/app/Profile/Ratings/index.tsx | 12 +- .../UserFeedbackModal/FeedbackForm.tsx | 53 +++++--- .../UserFeedbackModal/RatingFormBody.tsx | 126 +++++++++--------- .../UserFeedbackModal.module.scss | 83 ++++++++---- .../Class/Ratings/UserFeedbackModal/index.tsx | 10 +- .../UserFeedbackModal/useRatingFormState.ts | 4 + .../src/components/Class/Ratings/index.tsx | 3 +- .../Class/Ratings/ratingMutations.ts | 3 + apps/frontend/src/lib/api/ratings.ts | 2 + packages/common/src/models/index.ts | 1 + packages/common/src/models/review.ts | 55 ++++++++ packages/gql-typedefs/rating.ts | 1 + 14 files changed, 262 insertions(+), 117 deletions(-) create mode 100644 packages/common/src/models/review.ts diff --git a/apps/backend/src/modules/rating/controller.ts b/apps/backend/src/modules/rating/controller.ts index 32b53f238..52115c069 100644 --- a/apps/backend/src/modules/rating/controller.ts +++ b/apps/backend/src/modules/rating/controller.ts @@ -8,6 +8,7 @@ import { CourseModel, RatingModel, RatingType, + ReviewModel, SectionModel, } from "@repo/common/models"; import { METRIC_MAPPINGS, REQUIRED_METRICS } from "@repo/shared"; @@ -934,7 +935,8 @@ export const createRatings = async ( subject: string, courseNumber: string, classNumber: string, - metrics: MetricInput[] + metrics: MetricInput[], + review?: string ) => { if (!context.user._id) { throw new GraphQLError("Unauthorized", { @@ -1011,7 +1013,16 @@ export const createRatings = async ( ]); } - // Step 2: Create all new ratings and increment their aggregated counts + // Step 2: Upsert review if provided + if (review) { + await ReviewModel.findOneAndUpdate( + { createdBy: context.user._id, courseId }, + { text: review, classId, subject, courseNumber, semester, year, classNumber }, + { upsert: true, session } + ); + } + + // Step 3: Create all new ratings and increment their aggregated counts for (const metric of metrics) { await Promise.all([ RatingModel.create( @@ -1085,6 +1096,12 @@ export const deleteRatings = async ( const session = await connection.startSession(); try { await session.withTransaction(async () => { + // Delete review if exists + await ReviewModel.deleteOne( + { createdBy: context.user._id, courseId: existingRatings[0]?.courseId }, + { session } + ); + // Delete all ratings and decrement their aggregated counts for (const existingRating of existingRatings) { await Promise.all([ diff --git a/apps/backend/src/modules/rating/resolver.ts b/apps/backend/src/modules/rating/resolver.ts index 9c437acf0..2e8681e85 100644 --- a/apps/backend/src/modules/rating/resolver.ts +++ b/apps/backend/src/modules/rating/resolver.ts @@ -141,7 +141,7 @@ const resolvers: RatingModule.Resolvers = { Mutation: { createRatings: async ( _, - { year, semester, subject, courseNumber, classNumber, metrics }, + { year, semester, subject, courseNumber, classNumber, metrics, review }, context ) => { try { @@ -152,7 +152,8 @@ const resolvers: RatingModule.Resolvers = { subject, courseNumber, classNumber, - metrics + metrics, + review ?? undefined ); } catch (error: unknown) { // Re-throw GraphQLErrors as is diff --git a/apps/frontend/src/app/Profile/Ratings/index.tsx b/apps/frontend/src/app/Profile/Ratings/index.tsx index 42c6fcbff..00911c82a 100644 --- a/apps/frontend/src/app/Profile/Ratings/index.tsx +++ b/apps/frontend/src/app/Profile/Ratings/index.tsx @@ -194,7 +194,8 @@ export default function Ratings() { async ( metricValues: MetricData, termInfo: { semester: Semester; year: number }, - courseInfo: { subject: string; courseNumber: string; classNumber: string } + courseInfo: { subject: string; courseNumber: string; classNumber: string }, + review?: string ) => { if (!ratingForEdit) return; @@ -217,6 +218,7 @@ export default function Ratings() { number: courseInfo.classNumber, }, refetchQueries: buildRefetchQueries(refetchTarget), + review, }); }, [ratingForEdit, createRatingsMutation, buildRefetchQueries] @@ -226,7 +228,8 @@ export default function Ratings() { async ( metricValues: MetricData, termInfo: { semester: Semester; year: number }, - courseInfo: { subject: string; courseNumber: string; classNumber: string } + courseInfo: { subject: string; courseNumber: string; classNumber: string }, + review?: string ) => { const refetchTarget = { subject: courseInfo.subject, @@ -246,6 +249,7 @@ export default function Ratings() { number: courseInfo.classNumber, }, refetchQueries: buildRefetchQueries(refetchTarget), + review, }); }, [createRatingsMutation, buildRefetchQueries] @@ -336,8 +340,8 @@ export default function Ratings() { : null } availableTerms={availableTerms} - onSubmit={async (metricValues, termInfo, courseInfo) => { - await handleSubmitEdit(metricValues, termInfo, courseInfo); + onSubmit={async (metricValues, termInfo, courseInfo, review) => { + await handleSubmitEdit(metricValues, termInfo, courseInfo, review); }} initialUserClass={ratingForEdit} onSubmitPopupChange={setIsEditThankYouOpen} diff --git a/apps/frontend/src/components/Class/Ratings/UserFeedbackModal/FeedbackForm.tsx b/apps/frontend/src/components/Class/Ratings/UserFeedbackModal/FeedbackForm.tsx index 1e5696e50..be51bece7 100644 --- a/apps/frontend/src/components/Class/Ratings/UserFeedbackModal/FeedbackForm.tsx +++ b/apps/frontend/src/components/Class/Ratings/UserFeedbackModal/FeedbackForm.tsx @@ -37,7 +37,6 @@ interface RatingScaleProps { export function AttendanceForm({ metricData, setMetricData, - startQuestionNumber, }: AttendanceFormProps) { const handleAttendanceClickClick = ( type: MetricName, @@ -61,19 +60,15 @@ export function AttendanceForm({ ]; return ( -
- {ATTENDANCE_QUESTIONS.map(({ type, question }, index) => ( -
-
-

- {startQuestionNumber + index}. {question} -

- handleAttendanceClickClick(type, v)} - /> -
+
+ {ATTENDANCE_QUESTIONS.map(({ type, question }) => ( +
+

{question}

+ handleAttendanceClickClick(type, v)} + />
))}
@@ -114,7 +109,7 @@ export function RatingsForm({ ]; return ( -
+
{RATING_QUESTIONS.map( ({ type, question, leftLabel, rightLabel }, index) => ( void; +} + +export function ReviewForm({ review, setReview }: ReviewFormProps) { + return ( +
+
+

Write a Review

+