diff --git a/client/src/modules/Course/Components/Course.tsx b/client/src/modules/Course/Components/Course.tsx
index 2770fe09..e2386aff 100644
--- a/client/src/modules/Course/Components/Course.tsx
+++ b/client/src/modules/Course/Components/Course.tsx
@@ -27,6 +27,7 @@ import { Session } from '../../../session-store';
import { useAuthOptionalLogin } from '../../../auth/auth_utils';
import ReviewModal from './ReviewModal';
+import { compareSems } from 'common/CourseCard';
enum PageStatus {
Loading,
@@ -101,6 +102,14 @@ export const Course = () => {
return 0;
}
+ /**
+ * Sorts reviews based on descending semester (or by date if not available).
+ */
+ const sortBySem = (a: Review, b: Review) =>
+ b.semester && a.semester
+ ? compareSems(a.semester,b.semester)
+ : sortByDate(a,b)
+
/**
* Update state to conditionally render sticky bottom-right review button
*/
@@ -176,6 +185,8 @@ export const Course = () => {
setCourseReviews([...courseReviews].sort(sortByDate));
} else if (value === 'professor') {
setCourseReviews([...courseReviews].sort(sortByProf));
+ } else if (value === 'semester') {
+ setCourseReviews([...courseReviews].sort(sortBySem));
}
}
@@ -299,6 +310,7 @@ export const Course = () => {
+
diff --git a/client/src/modules/Course/Components/PreviewReviewCard.tsx b/client/src/modules/Course/Components/PreviewReviewCard.tsx
index 20a3054e..6e6ea83f 100644
--- a/client/src/modules/Course/Components/PreviewReviewCard.tsx
+++ b/client/src/modules/Course/Components/PreviewReviewCard.tsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { Review as ReviewType } from 'common';
+import { convertSemAbbreviation } from 'common/CourseCard'
import styles from '../Styles/ReviewCard.module.css';
import previewstyle from '../Styles/PreviewCard.module.css';
@@ -114,7 +115,11 @@ export default function PreviewReviewCard({
' ' +
courseNum?.toUpperCase() +
' | ' +
- professornames}
+ professornames +
+ (_review.semester
+ ? ', ' + convertSemAbbreviation(_review.semester)
+ : '')
+ }
>
);
@@ -193,14 +198,9 @@ export default function PreviewReviewCard({
{_review.grade && /^([^0-9]*)$/.test(_review.grade)
? _review.grade
- : 'N/A'}
+ : _review.writtenDuringSemester ? 'N/A (Review written during semester)' : 'N/A'
+ }
- {_review.semester ? (
- <>
- {' '} | {' '}
- {_review.semester}
- >
- ) : ''}
>
);
@@ -205,14 +211,9 @@ export default function ReviewCard({
{_review.grade && /^([^0-9]*)$/.test(_review.grade)
? _review.grade
- : 'N/A'}
+ : _review.writtenDuringSemester ? 'N/A (Review written during semester)' : 'N/A'
+ }
- {_review.semester ? (
- <>
- {' '} | {' '}
- {_review.semester}
- >
- ) : ''}
Major{' '}
diff --git a/client/src/modules/Course/Components/ReviewModal.tsx b/client/src/modules/Course/Components/ReviewModal.tsx
index 6d641434..33b3ea2d 100644
--- a/client/src/modules/Course/Components/ReviewModal.tsx
+++ b/client/src/modules/Course/Components/ReviewModal.tsx
@@ -5,6 +5,8 @@ import MultiSelect from './MultiSelect';
import SingleSelect from './SingleSelect';
import RatingInput from './RatingInput';
+import { SEMESTER_FORMAT } from 'common/CourseCard'
+
// CSS FILES
import styles from '../Styles/ReviewModal.module.css';
import closeIcon from '../../../assets/icons/X.svg';
@@ -13,6 +15,8 @@ import closeIcon from '../../../assets/icons/X.svg';
import majors from '../../Globals/majors';
import AnonymousWarning from './AnonymousWarning';
import { useAuthOptionalLogin } from '../../../auth/auth_utils';
+import { CURRENT_SEMESTER } from 'common/constants';
+import { compareWithCurrentSem } from 'common/CourseCard';
const ReviewModal = ({
open,
@@ -56,6 +60,7 @@ const ReviewModal = ({
const [selectedGrade, setSelectedGrade] = useState('');
const [selectedSem, setSelectedSem] = useState('');
const [reviewText, setReviewText] = useState('');
+ const [isCurrentSem, setIsCurrentSem] = useState(false);
const [overall, setOverall] = useState(3);
const [difficulty, setDifficulty] = useState(3);
@@ -75,8 +80,6 @@ const ReviewModal = ({
});
const [allowSubmit, setAllowSubmit] = useState(false);
- const SEMESTER_FORMAT: RegExp = new RegExp('^(SP|SU|WI|FA)\\d{2}$');
-
useEffect(() => {
if (!professorOptions.includes('Not Listed')) {
professorOptions.push('Not Listed');
@@ -84,12 +87,16 @@ const ReviewModal = ({
}, [professorOptions]);
useEffect(() => {
- setAllowSubmit(valid.professor && valid.semester && valid.major && valid.grade && valid.text);
+ setAllowSubmit(valid.professor && valid.semester && valid.major && (valid.grade !== isCurrentSem) && valid.text);
if (isLoggedIn) {
getNoReviews();
}
}, [valid]);
+ useEffect(() => {
+ if (isCurrentSem) onGradeChange('');
+ }, [isCurrentSem])
+
/**
* Determines if the current user has no reviews, so they should receive
* the anonymous modal
@@ -112,9 +119,16 @@ const ReviewModal = ({
}
function onSelectedSemChange(newSem: string) {
- setSelectedSem(newSem);
- if (SEMESTER_FORMAT.test(newSem)) setValid({ ...valid, semester: true });
- else setValid({ ...valid, semester: false });
+ if (newSem.includes("Current")) {
+ setIsCurrentSem(true);
+ setSelectedSem(newSem.substring(0,4));
+ } else {
+ setIsCurrentSem(false);
+ setSelectedSem(newSem)
+ }
+
+ let validSemFormat = SEMESTER_FORMAT.test(newSem.substring(0,4));
+ setValid({ ...valid, semester: validSemFormat });
}
function onMajorChange(newSelectedMajors: string[]) {
@@ -155,7 +169,8 @@ const ReviewModal = ({
isCovid: false,
grade: selectedGrade,
major: selectedMajors,
- semester: selectedSem
+ semester: selectedSem,
+ writtenDuringSemester: isCurrentSem,
};
submitReview(newReview);
}
@@ -244,17 +259,24 @@ const ReviewModal = ({
compareWithCurrentSem(sem) <= 0)
+ .map((sem) => sem === CURRENT_SEMESTER ? sem + " (Current)" : sem)
+ }
value={selectedSem}
onChange={onSelectedSemChange}
placeholder="Semester Taken"
/>
-
+ {!isCurrentSem &&
+
+ }
{
const [loading, setLoading] = useState(true);
@@ -91,6 +92,14 @@ const Profile = () => {
return 0
}
+ /**
+ * Sorts reviews based on descending semester.
+ */
+ const sortBySem = (a: ReviewType, b: ReviewType) =>
+ b.semester && a.semester
+ ? compareSems(a.semester,b.semester)
+ : sortByDate(a,b)
+
/**
* Hook that handles
* 1. Get + Set reviews
@@ -198,6 +207,8 @@ const Profile = () => {
setApprovedReviews([...approvedReviews].sort(sortByDate));
} else if (value === 'professor') {
setApprovedReviews([...approvedReviews].sort(sortByProf))
+ } else if (value === 'semester') {
+ setApprovedReviews([...approvedReviews].sort(sortBySem));
}
}
@@ -227,7 +238,9 @@ const Profile = () => {
-
My Reviews ({pendingReviews.length + approvedReviews.length})
+
+ My Reviews ({pendingReviews.length + approvedReviews.length})
+
@@ -249,15 +264,11 @@ const Profile = () => {
setHide={setHidePastReviews}
pendingReviews={pendingReviews}
/>
-
+
>
)}
{reviews.length > 0 && pendingReviews.length === 0 && (
-
+
)}
diff --git a/client/src/types.ts b/client/src/types.ts
index e6d44a8d..5bade22d 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -8,4 +8,5 @@ export type NewReview = {
grade: string;
major: string[];
semester: string;
+ writtenDuringSemester: boolean;
};
diff --git a/common/CourseCard.js b/common/CourseCard.js
index 4179439f..5675518a 100644
--- a/common/CourseCard.js
+++ b/common/CourseCard.js
@@ -2,6 +2,10 @@
Additonal functions used in the CourseCard component.
*/
+import { CURRENT_SEMESTER } from './constants';
+
+export const SEMESTER_FORMAT = new RegExp('^(SP|SU|WI|FA)\\d{2}$');
+
/**
* Helper function to convert semester abbreviations to a full word
*/
@@ -20,6 +24,55 @@ export function semAbbreviationToWord(sem) {
}
}
+/**
+ * Return full semester label -- eg. "FA20" --> "Fall 2020".
+ * Assumes 21st century (probably safe for now).
+ * @param sem abbreviated semester text
+ */
+export function convertSemAbbreviation(sem) {
+ return SEMESTER_FORMAT.test(sem)
+ ? semAbbreviationToWord(sem.substring(0, 2)) + " 20" + sem.substring(2)
+ : sem
+}
+
+/**
+ * Comparison function for two semesters.
+ * Returns -1 if semOne is less than semTwo
+ * 1 if semOne is greater than semTwo
+ * 0 otherwise (if they are equal)
+ */
+export function compareSems(semOne, semTwo) {
+ if (!SEMESTER_FORMAT.test(semOne) || !SEMESTER_FORMAT.test(semTwo)) {
+ throw new Error("When comparing semesters, both must be in abbreviated semester format.")
+ }
+
+ const semOrder = ['WI', 'SP', 'SU', 'FA'];
+ let semOneTermIndex = semOrder.indexOf(semOne.substring(0,2))
+ let semOneYear = parseInt(semOne.substring(2))
+ let semTwoTermIndex = semOrder.indexOf(semTwo.substring(0,2))
+ let semTwoYear = parseInt(semTwo.substring(2))
+
+ if (semOneYear < semTwoYear) return -1;
+ if (semOneYear > semTwoYear) return 1;
+ if (semOneTermIndex < semTwoTermIndex) return -1;
+ if (semOneTermIndex > semTwoTermIndex) return 1;
+ return 0;
+}
+
+/**
+ * Returns -1 if sem is less than current semester
+ * 1 if sem is greater than current semester
+ * 0 otherwise (if they are equal)
+ */
+export function compareWithCurrentSem(sem) {
+ if (!SEMESTER_FORMAT.test(sem)) throw new Error("Semester must be in abbreviated format.")
+ return compareSems(sem, CURRENT_SEMESTER)
+}
+
+export function isCurrentSem(sem) {
+ return compareWithCurrentSem(sem) === 0;
+}
+
/**
* Get a human-readable string representing a list of [up to] the last 2 semesters this class was offered.
*/
diff --git a/common/constants.ts b/common/constants.ts
new file mode 100644
index 00000000..03703245
--- /dev/null
+++ b/common/constants.ts
@@ -0,0 +1 @@
+export const CURRENT_SEMESTER = "WI25"
\ No newline at end of file
diff --git a/common/index.d.ts b/common/index.d.ts
index 7319f995..fe1b0626 100644
--- a/common/index.d.ts
+++ b/common/index.d.ts
@@ -56,6 +56,7 @@ export interface Review {
grade?: string;
major?: string[];
semester?: string;
+ writtenDuringSemester?: boolean;
}
export interface Professor {
diff --git a/server/db/schema.ts b/server/db/schema.ts
index 19ddfac0..cdcb6f07 100644
--- a/server/db/schema.ts
+++ b/server/db/schema.ts
@@ -123,6 +123,7 @@ const ReviewSchema = new Schema({
grade: { type: String },
major: { type: [String] },
semester: { type: String },
+ writtenDuringSemester: { type: Boolean },
// The following was a temporary field used to keep track of reviews for a contest
// The full functional code for counting reviews can be found on the following branch:
// review-counting-feature
diff --git a/server/src/review/review.controller.ts b/server/src/review/review.controller.ts
index 9ea508b5..988e2c33 100644
--- a/server/src/review/review.controller.ts
+++ b/server/src/review/review.controller.ts
@@ -201,6 +201,7 @@ export const insertNewReview = async ({
reported: 0,
professors: review.professors,
semester: review.semester,
+ writtenDuringSemester: review.writtenDuringSemester,
likes: 0,
isCovid: review.isCovid,
user: student._id,
diff --git a/server/src/review/review.ts b/server/src/review/review.ts
index b0e63ca4..5dd8e2c6 100644
--- a/server/src/review/review.ts
+++ b/server/src/review/review.ts
@@ -17,6 +17,7 @@ type ReviewEntity = {
grade: string;
major: string[];
semester: string;
+ writtenDuringSemester: boolean;
};
export class Review {
@@ -52,6 +53,8 @@ export class Review {
private semester: string;
+ private writtenDuringSemester: boolean;
+
constructor({
_id,
text,
@@ -68,7 +71,8 @@ export class Review {
grade,
major,
class: courseId,
- semester
+ semester,
+ writtenDuringSemester
}: ReviewEntity) {
this._id = _id;
this.text = text;
@@ -86,6 +90,7 @@ export class Review {
this.major = major;
this.class = courseId;
this.semester = semester ? semester : "";
+ this.writtenDuringSemester = writtenDuringSemester
this.validate();
}
@@ -115,6 +120,7 @@ export class Review {
grade: joi.optional(),
major: joi.optional(),
semester: joi.optional(),
+ writtenDuringSemester: joi.optional(),
});
const { error } = searchSchema.validate(this);
diff --git a/server/src/review/review.type.ts b/server/src/review/review.type.ts
index f289bb13..b3abb967 100644
--- a/server/src/review/review.type.ts
+++ b/server/src/review/review.type.ts
@@ -36,6 +36,7 @@ interface ReviewRequestType {
grade: string;
major: string[];
semester: string;
+ writtenDuringSemester: boolean;
}
export interface ReviewLikesRequestType {