Skip to content

Commit 8d49f60

Browse files
feat: add Course and Review data models; enhance CourseCard and ReviewForm components with detailed comments and validation logic
1 parent 3a97d1a commit 8d49f60

5 files changed

Lines changed: 122 additions & 14 deletions

File tree

edurate/frontend/src/components/CourseCard.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/**
2+
* CourseCard.jsx
3+
*
4+
* Displays a single course as a clickable card showing:
5+
* - Course name, department, and ID
6+
* - Average rating badge (color-coded)
7+
* - Number of reviews
8+
*
9+
* Clicking the card navigates to the course details page.
10+
*/
111
import { useNavigate } from 'react-router-dom';
212
import { getRatingColor } from '../utils/helpers';
313

edurate/frontend/src/components/ReviewForm.jsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
/**
2+
* ReviewForm.jsx
3+
*
4+
* Form component for submitting course reviews.
5+
* Allows users to:
6+
* - Select a star rating (1-5)
7+
* - Write a text review (minimum 10 characters)
8+
* - Optionally provide their name
9+
*
10+
* Validates input and displays success/error messages.
11+
*/
12+
113
import { useState } from 'react';
214
import { submitReview } from '../api';
15+
import { MIN_RATING, MAX_RATING, MIN_COMMENT_LENGTH, SUCCESS_MESSAGE_DURATION } from '../constants';
316

417
function ReviewForm({ courseId, onReviewSubmitted }) {
518
const [rating, setRating] = useState(0);
@@ -18,8 +31,13 @@ function ReviewForm({ courseId, onReviewSubmitted }) {
1831
return;
1932
}
2033

21-
if (comment.trim().length < 10) {
22-
setError('Comment must be at least 10 characters');
34+
if (rating < MIN_RATING || rating > MAX_RATING) {
35+
setError(`Rating must be between ${MIN_RATING} and ${MAX_RATING}`);
36+
return;
37+
}
38+
39+
if (comment.trim().length < MIN_COMMENT_LENGTH) {
40+
setError(`Comment must be at least ${MIN_COMMENT_LENGTH} characters`);
2341
return;
2442
}
2543

@@ -42,8 +60,8 @@ function ReviewForm({ courseId, onReviewSubmitted }) {
4260
// Notify parent to reload data
4361
onReviewSubmitted();
4462

45-
// Hide success message after 3 seconds
46-
setTimeout(() => setSuccess(false), 3000);
63+
// Hide success message after configured duration
64+
setTimeout(() => setSuccess(false), SUCCESS_MESSAGE_DURATION);
4765
} else {
4866
setError(result.error || 'Failed to submit review');
4967
}
@@ -54,6 +72,12 @@ function ReviewForm({ courseId, onReviewSubmitted }) {
5472
}
5573
};
5674

75+
// Generate array of star ratings based on constants
76+
const ratingOptions = Array.from(
77+
{ length: MAX_RATING },
78+
(_, i) => i + MIN_RATING
79+
);
80+
5781
return (
5882
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 sticky top-8">
5983
<h2 className="text-xl font-bold text-gray-900 mb-4">Write a Review</h2>
@@ -65,7 +89,7 @@ function ReviewForm({ courseId, onReviewSubmitted }) {
6589
Your Rating *
6690
</label>
6791
<div className="flex gap-2">
68-
{[1, 2, 3, 4, 5].map((star) => (
92+
{ratingOptions.map((star) => (
6993
<button
7094
key={star}
7195
type="button"
@@ -80,7 +104,7 @@ function ReviewForm({ courseId, onReviewSubmitted }) {
80104
</div>
81105
{rating > 0 && (
82106
<p className="text-sm text-gray-600 mt-1">
83-
You rated: {rating} out of 5
107+
You rated: {rating} out of {MAX_RATING}
84108
</p>
85109
)}
86110
</div>
@@ -99,7 +123,7 @@ function ReviewForm({ courseId, onReviewSubmitted }) {
99123
required
100124
/>
101125
<p className="text-xs text-gray-500 mt-1">
102-
{comment.length} characters (minimum 10)
126+
{comment.length} characters (minimum {MIN_COMMENT_LENGTH})
103127
</p>
104128
</div>
105129

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Course.js
3+
*
4+
* Data model for Course objects.
5+
*/
6+
export class Course {
7+
constructor(data) {
8+
this.id = data.id;
9+
this.name = data.Course_name;
10+
this.department = data.Department;
11+
this.courseId = data.Course_ID;
12+
this.rating = data.rating || 0;
13+
this.numReviews = data.numReviews || 0;
14+
this.createdAt = data.created_at;
15+
}
16+
17+
hasReviews() {
18+
return this.numReviews > 0;
19+
}
20+
21+
getRatingDisplay() {
22+
return this.rating === 0 ? 'N/A' : this.rating.toFixed(1);
23+
}
24+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Review.js
3+
*
4+
* Data model for Review objects.
5+
*/
6+
export class Review {
7+
constructor(data) {
8+
this.id = data.id;
9+
this.courseId = data.course_id;
10+
this.rating = data.rating;
11+
this.comment = data.comment;
12+
this.studentName = data.student_name || 'Anonymous';
13+
this.createdAt = new Date(data.created_at);
14+
}
15+
16+
getFormattedDate() {
17+
return this.createdAt.toLocaleDateString('en-US', {
18+
year: 'numeric',
19+
month: 'long',
20+
day: 'numeric'
21+
});
22+
}
23+
24+
isAnonymous() {
25+
return this.studentName === 'Anonymous';
26+
}
27+
}
Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1-
// Helper function to determine rating badge color based on score
1+
/**
2+
* helpers.js
3+
*
4+
* Utility functions for course filtering, searching, and rating display.
5+
* Uses constants from constants.js for consistent values across the app.
6+
*/
7+
8+
import { RATING_GREAT, RATING_GOOD, COLORS } from '../constants';
9+
10+
/**
11+
* Determine rating badge color based on score
12+
* @param {number} rating - Course rating (0-5)
13+
* @returns {string} Tailwind CSS color class
14+
*/
215
export const getRatingColor = (rating) => {
3-
if (rating >= 4.0) return 'bg-red-600'; // Red for great ratings
4-
if (rating >= 3.0) return 'bg-orange-500'; // Orange for good ratings
5-
if (rating > 0) return 'bg-gray-600'; // Gray for okay ratings
6-
return 'bg-gray-400'; // Light gray for no rating
16+
if (rating >= RATING_GREAT) return COLORS.GREAT;
17+
if (rating >= RATING_GOOD) return COLORS.GOOD;
18+
if (rating > 0) return COLORS.OKAY;
19+
return COLORS.NONE;
720
};
821

9-
// Filter courses based on search term and department
22+
/**
23+
* Filter courses based on search term and department
24+
* @param {Array} courses - Array of course objects
25+
* @param {string} searchTerm - Search query for course name or ID
26+
* @param {string} selectedDepartment - Department filter ('All' or department name)
27+
* @returns {Array} Filtered array of courses
28+
*/
1029
export const filterCourses = (courses, searchTerm, selectedDepartment) => {
1130
return courses.filter(course => {
1231
const matchesSearch =
@@ -21,7 +40,11 @@ export const filterCourses = (courses, searchTerm, selectedDepartment) => {
2140
});
2241
};
2342

24-
// Extract unique departments from courses
43+
/**
44+
* Extract unique departments from courses array
45+
* @param {Array} courses - Array of course objects
46+
* @returns {Array} Array of unique department names with 'All' prepended
47+
*/
2548
export const getUniqueDepartments = (courses) => {
2649
return ['All', ...new Set(courses.map(c => c.Department))];
2750
};

0 commit comments

Comments
 (0)