Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prof and course review section inaccurate metadata #350

Merged
merged 4 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cypress/e2e/2-reviews/course.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,18 +297,18 @@ context("Home", function () {
// rating section - // TODO make this dynamic
cy.get(
"[data-test=rating-average-rating] [data-test=stats-value]",
).should("contain.text", "3.50");
).should("contain.text", "3.73");
cy.get("[data-test=rating-interesting] [data-test=stats-value]").should(
"contain.text",
"10%",
"4%",
);
cy.get("[data-test=rating-practical] [data-test=stats-value]").should(
"contain.text",
"10%",
"4%",
);
cy.get(
"[data-test=rating-gained-new-skills] [data-test=stats-value]",
).should("contain.text", "10%");
).should("contain.text", "4%");
});

it("should display accurate review counts", function () {
Expand Down
8 changes: 4 additions & 4 deletions cypress/e2e/2-reviews/professor.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,18 +263,18 @@ context("Home", function () {
// rating section - // TODO make this dynamic
cy.get(
"[data-test=rating-average-rating] [data-test=stats-value]",
).should("contain.text", "4.40");
).should("contain.text", "4.25");
cy.get("[data-test=rating-engaging] [data-test=stats-value]").should(
"contain.text",
"60%",
"45%",
);
cy.get("[data-test=rating-fair-grading] [data-test=stats-value]").should(
"contain.text",
"50%",
"40%",
);
cy.get(
"[data-test=rating-effective-teaching] [data-test=stats-value]",
).should("contain.text", "50%");
).should("contain.text", "40%");
});

it("should display accurate review counts", function () {
Expand Down
34 changes: 17 additions & 17 deletions src/app/(school)/(reviews)/@rating/course/[code]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { api } from "@/common/tools/trpc/server";
import { RatingSection } from "@/modules/reviews/components/RatingSection";
import { ReviewLabelType } from "@prisma/client";
import calculateAverage from "@/common/functions/calculateAverage";
import calculateRatingItems from "@/modules/reviews/functions/calculateRatingItems";
import { auth } from "@/server/auth";
import { toTitleCase, formatPercentage } from "@/common/functions";

export default async function CourseRating({
params,
Expand All @@ -19,6 +18,7 @@ export default async function CourseRating({
if (!session) {
return (
<RatingSection
isLocked
headingRatingItem={{
label: "Average Rating",
rating: "-",
Expand All @@ -27,22 +27,23 @@ export default async function CourseRating({
label: label.name.replaceAll("_", " ").toLowerCase(),
rating: "-",
}))}
isLocked={!session}
/>
);
}

const professorSlugs = searchParams?.professor
? Array.isArray(searchParams.professor)
? searchParams.professor
: [searchParams.professor]
: [];
const apiParams = {
code: params.code,
...(professorSlugs.length > 0 && { slugs: professorSlugs }),
};
const { items: reviewsOfCourse } =
await api.reviews.getByCourseCodeProtected(apiParams);
if (reviewsOfCourse.length === 0) {

const { averageRating, reviewCount, reviewLabels } =
await api.reviews.getMetadataForCourse({
code: params.code,
withProfSlugs: professorSlugs.length > 0 ? professorSlugs : undefined,
});

if (reviewCount === 0) {
return (
<RatingSection
headingRatingItem={{
Expand All @@ -53,18 +54,17 @@ export default async function CourseRating({
/>
);
}

return (
<RatingSection
headingRatingItem={{
label: "Average Rating",
rating: calculateAverage(
reviewsOfCourse.map((review) => review.rating),
).toFixed(2),
rating: averageRating.toFixed(2),
}}
ratingItems={calculateRatingItems(
reviewsOfCourse,
validCourseReviewLabels,
)}
ratingItems={reviewLabels.map((label) => ({
label: toTitleCase(label.name),
rating: formatPercentage(label.count && label.count / reviewCount),
}))}
/>
);
}
44 changes: 14 additions & 30 deletions src/app/(school)/(reviews)/@rating/professor/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RatingSection } from "@/modules/reviews/components/RatingSection";
import formatPercentage from "@/common/functions/formatPercentage";
import { api } from "@/common/tools/trpc/server";
import { auth } from "@/server/auth";
import { toTitleCase, formatPercentage } from "@/common/functions";

export default async function ProfessorRating({
params,
Expand Down Expand Up @@ -34,21 +34,19 @@ export default async function ProfessorRating({
);
}

let courseCodes: string[] = [];
if (searchParams?.course) {
courseCodes = Array.isArray(searchParams.course)
const courseCodes = searchParams?.course
? Array.isArray(searchParams.course)
? searchParams.course
: [searchParams.course];
}
: [searchParams.course]
: [];

const { items: reviewsOfThisProf } = await api.reviews.getByProfSlugProtected(
{
const { averageRating, reviewCount, reviewLabels } =
await api.reviews.getMetadataForProf({
slug: params.slug,
courseCodes: courseCodes.length > 0 ? courseCodes : undefined,
},
);
withCourseCodes: courseCodes.length > 0 ? courseCodes : undefined,
});

if (reviewsOfThisProf.length === 0) {
if (reviewCount === 0) {
return (
<RatingSection
headingRatingItem={{
Expand All @@ -60,30 +58,16 @@ export default async function ProfessorRating({
);
}

const averageRating =
reviewsOfThisProf.reduce((total, next) => total + next.rating, 0) /
reviewsOfThisProf.length;

const ratingItems = validProfessorReviewLabels.map((label) => {
const reviewsWithThisLabel = reviewsOfThisProf.filter((r) =>
r.reviewLabels.map((rl) => rl.name).includes(label.name),
);

return {
label: label.name.replaceAll("_", " ").toLowerCase(),
rating: formatPercentage(
reviewsWithThisLabel.length / reviewsOfThisProf.length,
),
};
});

return (
<RatingSection
headingRatingItem={{
label: "Average Rating",
rating: averageRating.toFixed(2),
}}
ratingItems={ratingItems}
ratingItems={reviewLabels.map((label) => ({
label: toTitleCase(label.name),
rating: formatPercentage(label.count && label.count / reviewCount),
}))}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import React from "react";
import { BooksIcon } from "@/common/components/CustomIcon";
import { OgImage } from "@/modules/opengraph/components/OgImage";
import { api } from "@/common/tools/trpc/server";
import { toTitleCase } from "@/common/functions";
import formatPercentage from "@/common/functions/formatPercentage";
import { toTitleCase, formatPercentage } from "@/common/functions";

export const runtime = "nodejs";

Expand All @@ -24,7 +23,7 @@ export default async function Image({ params }: { params: { code: string } }) {
if (!course) return null;

const { averageRating, reviewCount, reviewLabels } =
await api.reviews.getMetadataByCourseCode({
await api.reviews.getMetadataForCourse({
code: courseCode,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import React from "react";
import { GraduationCapIcon } from "@/common/components/CustomIcon";
import { api } from "@/common/tools/trpc/server";
import { OgImage } from "@/modules/opengraph/components/OgImage";
import formatPercentage from "@/common/functions/formatPercentage";
import { toTitleCase } from "@/common/functions";
import { toTitleCase, formatPercentage } from "@/common/functions";

export const runtime = "nodejs";

Expand All @@ -24,7 +23,7 @@ export default async function Image({ params }: { params: { slug: string } }) {
if (!prof) return null;

const { averageRating, reviewCount, reviewLabels } =
await api.reviews.getMetadataByProfSlug({
await api.reviews.getMetadataForProf({
slug,
});
const courseCount = await api.courses.countByProfSlug({ slug });
Expand Down
2 changes: 1 addition & 1 deletion src/common/functions/calculateAverage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function calculateAverage(array: number[]): number {
export function calculateAverage(array: number[]): number {
const total = array.reduce((total: number, item: number) => total + item, 0);
return total / array.length;
}
2 changes: 1 addition & 1 deletion src/common/functions/formatPercentage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function formatPercentage(
export function formatPercentage(
amount: number,
options: Intl.NumberFormatOptions | undefined = undefined,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";

import { RatingSection } from "./RatingSection";
import formatPercentage from "@/common/functions/formatPercentage";
import { formatPercentage } from "@/common/functions";

const headingRatingItem = { label: "Average Rating", rating: 4.85 };
const ratingItems = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ReviewItemSkeleton,
} from "@/modules/reviews/components/ReviewItem";
import { AfterclassIcon } from "@/common/components/CustomIcon";
import { Button } from "@/common/components/Button";

export type ReviewItemLoaderHomeProps = {
variant: "home";
Expand Down Expand Up @@ -90,14 +91,33 @@ export const ReviewItemLoader = (props: ReviewItemLoaderProps) => {

return (
<>
{toShow.map((review) => (
<ReviewItem
key={review.id}
variant={props.variant}
review={review}
isLocked={!session}
/>
))}
{toShow.length > 0 ? (
toShow.map((review) => (
<ReviewItem
key={review.id}
variant={props.variant}
review={review}
isLocked={!session}
/>
))
) : (
<div className="w-full py-6 text-center text-xs text-text-em-mid md:text-sm">
<span>Oh no!</span> Looks like no one has reviewed yet.
<br />
Help us out by
<Button
as="a"
variant="link"
href="/submit"
className="mx-1 inline-flex h-fit pb-[1px] text-xs md:h-fit md:p-0 md:text-sm"
isResponsive
data-umami-event="no-review-cta"
>
writing one
</Button>
today ️🙈
</div>
)}
{status === "authenticated" && hasNextPage && (
<InView
as="div"
Expand Down
2 changes: 1 addition & 1 deletion src/modules/reviews/functions/calculateRatingItems.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Review } from "@/modules/reviews/types";
import { type Labels } from "@prisma/client";
import formatPercentage from "@/common/functions/formatPercentage";
import { formatPercentage } from "@/common/functions";

export default function calculateRatingItems(
reviews: Review[],
Expand Down
36 changes: 22 additions & 14 deletions src/server/api/routers/reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,17 +492,23 @@ export const reviewsRouter = createTRPCRouter({
}),
),

getMetadataByProfSlug: publicProcedure
getMetadataForProf: publicProcedure
.input(
z.object({
slug: z.string(),
withCourseCodes: z.string().array().optional(),
}),
)
.query(async ({ ctx, input }) => {
const reviewWhereInput = {
reviewedProfessor: { slug: input.slug },
...(input.withCourseCodes && {
reviewedCourse: { code: { in: input.withCourseCodes } },
}),
} satisfies Prisma.ReviewsWhereInput;

const reviewsMetadataForThisProf = await ctx.db.reviews.aggregate({
where: {
reviewedProfessor: { slug: input.slug },
},
where: reviewWhereInput,
_avg: {
rating: true,
},
Expand Down Expand Up @@ -531,9 +537,7 @@ export const reviewsRouter = createTRPCRouter({
labelId: true,
},
where: {
review: {
reviewedProfessor: { slug: input.slug },
},
review: reviewWhereInput,
},
},
);
Expand All @@ -556,17 +560,23 @@ export const reviewsRouter = createTRPCRouter({
};
}),

getMetadataByCourseCode: publicProcedure
getMetadataForCourse: publicProcedure
.input(
z.object({
code: z.string(),
withProfSlugs: z.string().array().optional(),
}),
)
.query(async ({ ctx, input }) => {
const reviewWhereInput = {
reviewedCourse: { code: input.code },
...(input.withProfSlugs && {
reviewedProfessor: { slug: { in: input.withProfSlugs } },
}),
} satisfies Prisma.ReviewsWhereInput;

const reviewsMetadataForThisCourse = await ctx.db.reviews.aggregate({
where: {
reviewedCourse: { code: input.code },
},
where: reviewWhereInput,
_avg: {
rating: true,
},
Expand Down Expand Up @@ -595,9 +605,7 @@ export const reviewsRouter = createTRPCRouter({
labelId: true,
},
where: {
review: {
reviewedCourse: { code: input.code },
},
review: reviewWhereInput,
},
});

Expand Down
Loading